docs/references/messaging/message-system.md
This document describes the Cherry Studio message system architecture, including message lifecycle, state management, and operation interfaces.
This file defines the Redux Slice for managing all MessageBlock entities in the application. It uses Redux Toolkit's createSlice and createEntityAdapter to efficiently handle normalized state, providing a set of actions and selectors for interacting with message block data.
MessageBlocks. A MessageBlock represents different content units within a message (e.g., text, code, images, citations).createEntityAdapter to store MessageBlock data in a normalized structure ({ ids: [], entities: {} }), improving performance and simplifying update logic.createSlice): Redux Toolkit's core API for creating Redux modules containing reducer logic, action creators, and initial state.createEntityAdapter): A Redux Toolkit utility that simplifies CRUD (Create, Read, Update, Delete) operations on normalized data. It auto-generates reducer functions and selectors.The messageBlocks slice state structure is defined by createEntityAdapter, roughly:
{
ids: string[]; // Ordered list of all MessageBlock IDs
entities: { [id: string]: MessageBlock }; // Dictionary mapping ID to MessageBlock objects
loadingState: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
}
The slice exports the following actions (auto-generated by createSlice and createEntityAdapter or custom):
upsertOneBlock(payload: MessageBlock): Add a new MessageBlock or update an existing one. If the id in the payload already exists, it performs an update; otherwise it inserts.
upsertManyBlocks(payload: MessageBlock[]): Add or update multiple MessageBlocks. Commonly used for batch loading data (e.g., loading all message blocks for a Topic).
removeOneBlock(payload: string): Remove a single MessageBlock by the provided id.
removeManyBlocks(payload: string[]): Remove multiple MessageBlocks by the provided id array. Commonly used when deleting messages or clearing a Topic.
removeAllBlocks(): Remove all MessageBlock entities from state.
updateOneBlock(payload: { id: string; changes: Partial<MessageBlock> }): Update an existing MessageBlock. The payload must contain the block's id and a changes object with the fields to modify.
setMessageBlocksLoading(payload: 'idle' | 'loading'): (Custom) Set the loadingState property.
setMessageBlocksError(payload: string): (Custom) Set loadingState to 'failed' and record the error message.
Usage Example (in Thunks or other dispatch locations):
import { upsertOneBlock, removeManyBlocks, updateOneBlock } from './messageBlock'
import store from './store'
// Add or update a block
const newBlock: MessageBlock = { /* ... block data ... */ }
store.dispatch(upsertOneBlock(newBlock))
// Update a block's content
store.dispatch(updateOneBlock({ id: blockId, changes: { content: 'New content' } }))
// Delete multiple blocks
const blockIdsToRemove = ['id1', 'id2']
store.dispatch(removeManyBlocks(blockIdsToRemove))
The slice exports base selectors generated by createEntityAdapter, accessed through messageBlocksSelectors:
messageBlocksSelectors.selectIds(state: RootState): string[]: Returns an array of all block IDs.messageBlocksSelectors.selectEntities(state: RootState): { [id: string]: MessageBlock }: Returns a dictionary mapping block IDs to block objects.messageBlocksSelectors.selectAll(state: RootState): MessageBlock[]: Returns an array of all block objects.messageBlocksSelectors.selectTotal(state: RootState): number: Returns the total number of blocks.messageBlocksSelectors.selectById(state: RootState, id: string): MessageBlock | undefined: Returns a single block by ID, or undefined if not found.Additionally, a custom memoized selector is provided:
selectFormattedCitationsByBlockId(state: RootState, blockId: string | undefined): Citation[]:
blockId.CITATION type, it extracts and formats citation information (from web search, knowledge base, etc.), deduplicates, renumbers, and returns a Citation[] array for UI display.[] if the block doesn't exist or the type doesn't match.Usage Example (in React components or useSelector):
import { useSelector } from 'react-redux'
import { messageBlocksSelectors, selectFormattedCitationsByBlockId } from './messageBlock'
import type { RootState } from './store'
// Get all blocks
const allBlocks = useSelector(messageBlocksSelectors.selectAll)
// Get a specific block by ID
const specificBlock = useSelector((state: RootState) => messageBlocksSelectors.selectById(state, someBlockId))
// Get formatted citations for a citation block
const formattedCitations = useSelector((state: RootState) => selectFormattedCitationsByBlockId(state, citationBlockId))
The messageBlock.ts slice typically works closely with Thunks in messageThunk.ts. Thunks handle async logic (API calls, database operations) and dispatch messageBlock slice actions to update state as needed. For example, when messageThunk receives a streaming response, it dispatches upsertOneBlock or updateOneBlock to update the corresponding MessageBlock in real-time. Similarly, message deletion Thunks dispatch removeManyBlocks.
Understanding that messageBlock.ts manages the state itself while messageThunk.ts handles the async workflows that trigger state changes is essential for maintaining a clear application architecture.
This file contains core Thunk Action Creators for managing message flows, handling assistant interactions, and synchronizing Redux state with the IndexedDB database. It primarily operates on Message and MessageBlock objects.
MessageBlocks.MessageBlocks (text, thinking process, tool calls, citations, images, errors, translations, etc.).sendMessage(userMessage, userMessageBlocks, assistant, topicId)@mentions to determine single-model or multi-model response.fetchAndProcessAssistantResponseImpl for the topicId to get actual responses.fetchAndProcessAssistantResponseImpl(dispatch, getState, topicId, assistant, assistantMessage)fetchChatCompletion API service.createStreamProcessor.onTextChunk, onThinkingChunk, onToolCallComplete, onImageGenerated, onError, onComplete, etc.).UNKNOWN blocks from stream events, real-time creates/updates MAIN_TEXT and THINKING blocks with throttled updates, creates TOOL, CITATION, IMAGE, ERROR blocks as needed.loadTopicMessagesThunk(topicId, forceReload)MessageBlocks for a topic from the database.Topic and messages from DB, get all related MessageBlocks, upsert blocks and messages to Redux.deleteSingleMessageThunk(topicId, messageId): Delete a single message and all its blocks.deleteMessageGroupThunk(topicId, askId): Delete a user message and all related assistant responses with their blocks.clearTopicMessagesThunk(topicId): Clear all messages and blocks under a topic.resendMessageThunk(topicId, userMessageToResend, assistant): Resend a user message. Resets all associated assistant responses (clears blocks, marks as PENDING), then re-requests generation.resendUserMessageWithEditThunk(...): Update user message's MAIN_TEXT block content, then call resendMessageThunk.regenerateAssistantResponseThunk(...): Regenerate a single assistant response. Resets the message (clears blocks, marks as PENDING), then re-requests generation.appendAssistantResponseThunk(topicId, existingAssistantMessageId, newModel, assistant)newModel (with the same askId), then queues fetchAndProcessAssistantResponseImpl.cloneMessagesToNewTopicThunk(sourceTopicId, branchPointIndex, newTopic)askId relationships, updates file reference counts.initiateTranslationThunk(messageId, topicId, targetLanguage, sourceBlockId?, sourceLanguage?)TRANSLATION type MessageBlock for a message.STREAMING status TranslationMessageBlock, adds to Redux and DB, updates the message's blocks list.saveMessageAndBlocksToDB, throttledBlockDbUpdate, etc. to interact with IndexedDB.getTopicQueue): Uses AsyncQueue to ensure operations on the same topic execute sequentially, avoiding race conditions.throttle): Uses throttle from es-toolkit/compat to optimize frequent block updates (text, thinking) during streaming, reducing Redux dispatch and DB write frequency.fetchAndProcessAssistantResponseImpl (especially onError) handle stream processing and API call errors, creating ERROR type blocks.This file defines a custom React Hook called useMessageOperations. Its main purpose is to provide React components with a convenient interface for performing various message operations related to a specific Topic. It encapsulates the logic for calling Redux Thunks and Actions, simplifying component interaction with message data.
dispatch or Thunks.topic object.import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
import type { Topic, Message, Assistant, Model } from '@renderer/types'
function MyComponent({ currentTopic, currentAssistant }: { currentTopic: Topic; currentAssistant: Assistant }) {
const {
deleteMessage,
resendMessage,
regenerateAssistantMessage,
appendAssistantResponse,
getTranslationUpdater,
createTopicBranch,
} = useMessageOperations(currentTopic)
// Use the operation functions in your component
}
deleteMessage(id: string): Delete a single message by ID.deleteGroupMessages(askId: string): Delete a group of messages associated with an askId.editMessage(messageId: string, updates: Partial<Message>): Update partial attributes of a message.resendMessage(message: Message, assistant: Assistant): Resend a user message, triggering regeneration of all associated assistant responses.resendUserMessageWithEdit(message: Message, editedContent: string, assistant: Assistant): Resend after editing user message text.clearTopicMessages(_topicId?: string): Clear all messages in the current (or specified) topic.createNewContext(): Emit a global event to clear display and prepare a new context.displayCount: Current displayCount value from Redux store.pauseMessages(): Abort ongoing message generation for the current topic.resumeMessage(message: Message, assistant: Assistant): Resume/resend a user message.regenerateAssistantMessage(message: Message, assistant: Assistant): Regenerate a specific assistant message response.appendAssistantResponse(existingAssistantMessage: Message, newModel: Model, assistant: Assistant): Append a new assistant response using a different model.getTranslationUpdater(messageId, targetLanguage, sourceBlockId?, sourceLanguage?): Get a function for progressively updating a translation block's content. Returns an async updater function or null on failure.createTopicBranch(sourceTopicId, branchPointIndex, newTopic): Clone messages from a source topic to a new topic (which must already exist in Redux and DB).useTopicMessages(topic: Topic): Get the message list for a topic.useTopicLoading(topic: Topic): Get the loading state for a topic.These hooks can be combined with useMessageOperations for convenient data access and operations within components.