Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: suyao <sy20010504@gmail.com>
14 KiB
Message System
This document describes the Cherry Studio message system architecture, including message lifecycle, state management, and operation interfaces.
Message Lifecycle
messageBlock.ts Usage Guide
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.
Core Goals
- State Management: Centrally manage the state of all
MessageBlocks. AMessageBlockrepresents different content units within a message (e.g., text, code, images, citations). - Normalization: Uses
createEntityAdapterto storeMessageBlockdata in a normalized structure ({ ids: [], entities: {} }), improving performance and simplifying update logic. - Predictability: Provides explicit actions to modify state and selectors to safely access state.
Key Concepts
- Slice (
createSlice): Redux Toolkit's core API for creating Redux modules containing reducer logic, action creators, and initial state. - Entity Adapter (
createEntityAdapter): A Redux Toolkit utility that simplifies CRUD (Create, Read, Update, Delete) operations on normalized data. It auto-generates reducer functions and selectors. - Selectors: Functions for deriving and computing data from the Redux store. Selectors can be memoized for improved performance.
State Structure
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;
}
Actions
The slice exports the following actions (auto-generated by createSlice and createEntityAdapter or custom):
-
upsertOneBlock(payload: MessageBlock): Add a newMessageBlockor update an existing one. If theidin the payload already exists, it performs an update; otherwise it inserts. -
upsertManyBlocks(payload: MessageBlock[]): Add or update multipleMessageBlocks. Commonly used for batch loading data (e.g., loading all message blocks for a Topic). -
removeOneBlock(payload: string): Remove a singleMessageBlockby the providedid. -
removeManyBlocks(payload: string[]): Remove multipleMessageBlocks by the providedidarray. Commonly used when deleting messages or clearing a Topic. -
removeAllBlocks(): Remove allMessageBlockentities from state. -
updateOneBlock(payload: { id: string; changes: Partial<MessageBlock> }): Update an existingMessageBlock. Thepayloadmust contain the block'sidand achangesobject with the fields to modify. -
setMessageBlocksLoading(payload: 'idle' | 'loading'): (Custom) Set theloadingStateproperty. -
setMessageBlocksError(payload: string): (Custom) SetloadingStateto'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))
Selectors
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, orundefinedif not found.
Additionally, a custom memoized selector is provided:
selectFormattedCitationsByBlockId(state: RootState, blockId: string | undefined): Citation[]:- Takes a
blockId. - If the corresponding block is of
CITATIONtype, it extracts and formats citation information (from web search, knowledge base, etc.), deduplicates, renumbers, and returns aCitation[]array for UI display. - Returns an empty array
[]if the block doesn't exist or the type doesn't match. - This selector encapsulates complex logic for handling different citation sources (Gemini, OpenAI, OpenRouter, Zhipu, etc.).
- Takes a
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))
Integration
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.
messageThunk.ts Usage Guide
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.
Core Functions
- Send/Receive Messages: Handle user message sending, trigger assistant responses, stream-process returned data, and parse it into different
MessageBlocks. - State Management: Ensure consistency between message and message block state in the Redux store and persisted data in IndexedDB.
- Message Operations: Provide deletion, resend, regeneration, edit-and-resend, append response, clone, and other message lifecycle management functions.
- Block Processing: Dynamically create, update, and save various types of
MessageBlocks (text, thinking process, tool calls, citations, images, errors, translations, etc.).
Key Thunks
1. sendMessage(userMessage, userMessageBlocks, assistant, topicId)
- Purpose: Send a new user message.
- Flow:
- Save user message and its blocks to Redux and DB.
- Check
@mentionsto determine single-model or multi-model response. - Create assistant message stub(s).
- Add stubs to Redux and DB.
- Queue
fetchAndProcessAssistantResponseImplfor thetopicIdto get actual responses.
2. fetchAndProcessAssistantResponseImpl(dispatch, getState, topicId, assistant, assistantMessage)
- Purpose: (Internal) Core logic for fetching and processing a single assistant response.
- Flow:
- Set Topic loading state.
- Prepare context messages.
- Call
fetchChatCompletionAPI service. - Process streaming response using
createStreamProcessor. - Handle different event types via callbacks (
onTextChunk,onThinkingChunk,onToolCallComplete,onImageGenerated,onError,onComplete, etc.).
- Block handling: Creates initial
UNKNOWNblocks from stream events, real-time creates/updatesMAIN_TEXTandTHINKINGblocks with throttled updates, createsTOOL,CITATION,IMAGE,ERRORblocks as needed.
3. loadTopicMessagesThunk(topicId, forceReload)
- Purpose: Load all messages and associated
MessageBlocks for a topic from the database. - Flow: Fetch
Topicand messages from DB, get all relatedMessageBlocks, upsert blocks and messages to Redux.
4. Delete Thunks
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.
5. Resend/Regenerate Thunks
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'sMAIN_TEXTblock content, then callresendMessageThunk.regenerateAssistantResponseThunk(...): Regenerate a single assistant response. Resets the message (clears blocks, marks as PENDING), then re-requests generation.
6. appendAssistantResponseThunk(topicId, existingAssistantMessageId, newModel, assistant)
- Purpose: Append a new assistant response using a different model for the same user question.
- Creates a new assistant message stub using
newModel(with the sameaskId), then queuesfetchAndProcessAssistantResponseImpl.
7. cloneMessagesToNewTopicThunk(sourceTopicId, branchPointIndex, newTopic)
- Purpose: Clone messages (and their blocks) from a source topic up to a given index into a new topic.
- Generates new UUIDs for all cloned messages and blocks, maps
askIdrelationships, updates file reference counts.
8. initiateTranslationThunk(messageId, topicId, targetLanguage, sourceBlockId?, sourceLanguage?)
- Purpose: Create an initial
TRANSLATIONtypeMessageBlockfor a message. - Creates a
STREAMINGstatusTranslationMessageBlock, adds to Redux and DB, updates the message'sblockslist.
Internal Mechanisms
- Database Interaction: Uses helper functions like
saveMessageAndBlocksToDB,throttledBlockDbUpdate, etc. to interact with IndexedDB. - State Synchronization: Thunks coordinate data consistency between the Redux Store and IndexedDB.
- Queue (
getTopicQueue): UsesAsyncQueueto ensure operations on the same topic execute sequentially, avoiding race conditions. - Throttling (
throttle): Usesthrottlefrom es-toolkit/compat to optimize frequent block updates (text, thinking) during streaming, reducing Redux dispatch and DB write frequency. - Error Handling: Callbacks in
fetchAndProcessAssistantResponseImpl(especiallyonError) handle stream processing and API call errors, creatingERRORtype blocks.
useMessageOperations.ts Usage Guide
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.
Core Goals
- Encapsulation: Wrap complex message operation logic (delete, resend, regenerate, edit, translate, etc.) in easy-to-use functions.
- Simplification: Allow components to call operation functions directly without interacting with Redux
dispatchor Thunks. - Context Association: All operations are associated with the provided
topicobject.
How to Use
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
}
Return Values
deleteMessage(id: string): Delete a single message by ID.deleteGroupMessages(askId: string): Delete a group of messages associated with anaskId.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: CurrentdisplayCountvalue 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 ornullon failure.createTopicBranch(sourceTopicId, branchPointIndex, newTopic): Clone messages from a source topic to a new topic (which must already exist in Redux and DB).
Related Hooks
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.
