# Message System This document describes the Cherry Studio message system architecture, including message lifecycle, state management, and operation interfaces. ## Message Lifecycle ![Message Lifecycle](../../assets/images/message-lifecycle.png) --- # 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 `MessageBlock`s. A `MessageBlock` represents different content units within a message (e.g., text, code, images, citations). - **Normalization**: Uses `createEntityAdapter` to store `MessageBlock` data 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: ```typescript { 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 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 `MessageBlock`s. 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 `MessageBlock`s 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 })`**: 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):** ```typescript 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, or `undefined` if 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 `CITATION` type, it extracts and formats citation information (from web search, knowledge base, etc.), deduplicates, renumbers, and returns a `Citation[]` 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.). **Usage Example (in React components or `useSelector`):** ```typescript 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 1. **Send/Receive Messages**: Handle user message sending, trigger assistant responses, stream-process returned data, and parse it into different `MessageBlock`s. 2. **State Management**: Ensure consistency between message and message block state in the Redux store and persisted data in IndexedDB. 3. **Message Operations**: Provide deletion, resend, regeneration, edit-and-resend, append response, clone, and other message lifecycle management functions. 4. **Block Processing**: Dynamically create, update, and save various types of `MessageBlock`s (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 `@mentions` to determine single-model or multi-model response. - Create assistant message stub(s). - Add stubs to Redux and DB. - Queue `fetchAndProcessAssistantResponseImpl` for the `topicId` to 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 `fetchChatCompletion` API service. - Process streaming response using `createStreamProcessor`. - Handle different event types via callbacks (`onTextChunk`, `onThinkingChunk`, `onToolCallComplete`, `onImageGenerated`, `onError`, `onComplete`, etc.). - **Block handling**: Creates initial `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. ### 3. `loadTopicMessagesThunk(topicId, forceReload)` - **Purpose**: Load all messages and associated `MessageBlock`s for a topic from the database. - **Flow**: Fetch `Topic` and messages from DB, get all related `MessageBlock`s, 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'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. ### 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 same `askId`), then queues `fetchAndProcessAssistantResponseImpl`. ### 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 `askId` relationships, updates file reference counts. ### 8. `initiateTranslationThunk(messageId, topicId, targetLanguage, sourceBlockId?, sourceLanguage?)` - **Purpose**: Create an initial `TRANSLATION` type `MessageBlock` for a message. - Creates a `STREAMING` status `TranslationMessageBlock`, adds to Redux and DB, updates the message's `blocks` list. ## 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`)**: Uses `AsyncQueue` to ensure operations on the same topic execute sequentially, avoiding race conditions. - **Throttling (`throttle`)**: Uses `throttle` from es-toolkit/compat to optimize frequent block updates (text, thinking) during streaming, reducing Redux dispatch and DB write frequency. - **Error Handling**: Callbacks in `fetchAndProcessAssistantResponseImpl` (especially `onError`) handle stream processing and API call errors, creating `ERROR` type 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 `dispatch` or Thunks. - **Context Association**: All operations are associated with the provided `topic` object. ## How to Use ```typescript 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 an `askId`. - **`editMessage(messageId: string, updates: Partial)`**: 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). ## 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.