Refactor agent session messages to use shared hook and implement batch deletion

- Replace manual Redux logic with `useTopicMessages` hook for consistent message loading behavior
- Add `deleteMessages` method to message data sources with proper block and file cleanup
- Update `DbService` to delegate batch deletion to appropriate data source implementations
This commit is contained in:
suyao 2025-09-22 22:50:45 +08:00
parent e5aa58722c
commit f533c1a2ca
No known key found for this signature in database
6 changed files with 40 additions and 75 deletions

View File

@ -1,13 +1,11 @@
import { loggerService } from '@logger'
import ContextMenu from '@renderer/components/ContextMenu'
import { useSession } from '@renderer/hooks/agents/useSession'
import { useTopicMessages } from '@renderer/hooks/useMessageOperations'
import { getGroupedMessages } from '@renderer/services/MessagesService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { selectMessagesForTopic } from '@renderer/store/newMessage'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { Topic } from '@renderer/types'
import { type Topic, TopicType } from '@renderer/types'
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
import { memo, useEffect, useMemo } from 'react'
import { memo, useMemo } from 'react'
import styled from 'styled-components'
import MessageGroup from './MessageGroup'
@ -22,23 +20,10 @@ type Props = {
}
const AgentSessionMessages: React.FC<Props> = ({ agentId, sessionId }) => {
const dispatch = useAppDispatch()
const { session } = useSession(agentId, sessionId)
const sessionTopicId = useMemo(() => buildAgentSessionTopicId(sessionId), [sessionId])
const messages = useAppSelector((state) => selectMessagesForTopic(state, sessionTopicId))
// Load messages when session changes or when messages are empty
useEffect(() => {
if (sessionId) {
// Only load if we don't have messages yet
// This prevents overwriting messages that were just added
const hasMessages = messages && messages.length > 0
if (!hasMessages) {
logger.info('Loading messages for agent session', { sessionId })
dispatch(loadTopicMessagesThunk(sessionTopicId, false)) // Don't force reload if we have messages in Redux
}
}
}, [dispatch, sessionId, sessionTopicId, messages?.length])
// Use the same hook as Messages.tsx for consistent behavior
const messages = useTopicMessages(sessionTopicId)
const displayMessages = useMemo(() => {
if (!messages || messages.length === 0) return []
@ -58,6 +43,7 @@ const AgentSessionMessages: React.FC<Props> = ({ agentId, sessionId }) => {
const derivedTopic = useMemo<Topic>(
() => ({
id: sessionTopicId,
type: TopicType.Session,
assistantId: sessionAssistantId,
name: sessionName,
createdAt: sessionCreatedAt,

View File

@ -373,6 +373,15 @@ export class AgentMessageDataSource implements MessageDataSource {
// 2. Or just hide from UI without actual deletion
}
async deleteMessages(topicId: string, _messageIds: string[]): Promise<void> {
// Agent session messages cannot be deleted in batch
logger.warn(`deleteMessages called for agent session ${topicId}, operation not supported`)
// In a full implementation, you might want to:
// 1. Implement batch soft delete in backend
// 2. Update local state accordingly
}
async deleteMessagesByAskId(topicId: string, _askId: string): Promise<void> {
// Agent session messages cannot be deleted
logger.warn(`deleteMessagesByAskId called for agent session ${topicId}, operation not supported`)

View File

@ -86,9 +86,9 @@ class DbService implements MessageDataSource {
return source.deleteMessage(topicId, messageId)
}
async deleteMessagesByAskId(topicId: string, askId: string): Promise<void> {
async deleteMessages(topicId: string, messageIds: string[]): Promise<void> {
const source = this.getDataSource(topicId)
return source.deleteMessagesByAskId(topicId, askId)
return source.deleteMessages(topicId, messageIds)
}
// ============ Block Operations ============

View File

@ -203,39 +203,48 @@ export class DexieMessageDataSource implements MessageDataSource {
}
}
async deleteMessagesByAskId(topicId: string, askId: string): Promise<void> {
async deleteMessages(topicId: string, messageIds: string[]): Promise<void> {
try {
await db.transaction('rw', db.topics, db.message_blocks, db.files, async () => {
const topic = await db.topics.get(topicId)
if (!topic) return
// Find all messages with the given askId
const messagesToDelete = topic.messages.filter((m) => m.askId === askId || m.id === askId)
const blockIdsToDelete = messagesToDelete.flatMap((m) => m.blocks || [])
// Collect all block IDs from messages to be deleted
const allBlockIds: string[] = []
const messagesToDelete: Message[] = []
for (const messageId of messageIds) {
const message = topic.messages.find((m) => m.id === messageId)
if (message) {
messagesToDelete.push(message)
if (message.blocks && message.blocks.length > 0) {
allBlockIds.push(...message.blocks)
}
}
}
// Delete blocks and handle files
if (blockIdsToDelete.length > 0) {
const blocks = await db.message_blocks.where('id').anyOf(blockIdsToDelete).toArray()
if (allBlockIds.length > 0) {
const blocks = await db.message_blocks.where('id').anyOf(allBlockIds).toArray()
const files = blocks
.filter((block) => block.type === 'file' || block.type === 'image')
.map((block: any) => block.file)
.filter((file) => file !== undefined)
// Clean up files
if (!isEmpty(files)) {
await Promise.all(files.map((file) => FileManager.deleteFile(file.id, false)))
}
await db.message_blocks.bulkDelete(blockIdsToDelete)
await db.message_blocks.bulkDelete(allBlockIds)
}
// Filter out deleted messages
const remainingMessages = topic.messages.filter((m) => m.askId !== askId && m.id !== askId)
// Remove messages from topic
const remainingMessages = topic.messages.filter((m) => !messageIds.includes(m.id))
await db.topics.update(topicId, { messages: remainingMessages })
})
store.dispatch(updateTopicUpdatedAt({ topicId }))
} catch (error) {
logger.error(`Failed to delete messages with askId ${askId} from topic ${topicId}:`, error as Error)
logger.error(`Failed to delete messages from topic ${topicId}:`, error as Error)
throw error
}
}

View File

@ -64,9 +64,9 @@ export interface MessageDataSource {
deleteMessage(topicId: string, messageId: string): Promise<void>
/**
* Delete messages by askId (user query + assistant responses)
* Delete multiple messages and their blocks
*/
deleteMessagesByAskId(topicId: string, askId: string): Promise<void>
deleteMessages(topicId: string, messageIds: string[]): Promise<void>
// ============ Block Operations ============
/**

View File

@ -5,10 +5,7 @@
import { loggerService } from '@logger'
import { dbService } from '@renderer/services/db'
import type { Topic } from '@renderer/types'
import { TopicType } from '@renderer/types'
import type { Message, MessageBlock } from '@renderer/types/newMessage'
import { isAgentSessionTopicId } from '@renderer/utils/agentSession'
import type { AppDispatch, RootState } from '../index'
import { upsertManyBlocks } from '../messageBlock'
@ -80,42 +77,6 @@ export const getRawTopicV2 = async (topicId: string): Promise<{ id: string; mess
// Phase 2.2 - Batch 2: Helper functions
// =================================================================
/**
* Get a full topic object with type information
* This builds on getRawTopicV2 to provide additional metadata
*/
export const getTopicV2 = async (topicId: string): Promise<Topic | undefined> => {
try {
const rawTopic = await dbService.getRawTopic(topicId)
if (!rawTopic) {
logger.info('Topic not found', { topicId })
return undefined
}
// Construct the full Topic object
const topic: Topic = {
id: rawTopic.id,
type: isAgentSessionTopicId(topicId) ? TopicType.Session : TopicType.Chat,
messages: rawTopic.messages,
assistantId: '', // These fields would need to be fetched from appropriate source
name: '',
createdAt: Date.now(),
updatedAt: Date.now()
}
logger.info('Retrieved topic with type via DbService', {
topicId,
type: topic.type,
messageCount: topic.messages.length
})
return topic
} catch (error) {
logger.error('Failed to get topic:', { topicId, error })
return undefined
}
}
/**
* Update file reference count
* Only applies to Dexie data source, no-op for agent sessions