diff --git a/locales/en-US/chat.json b/locales/en-US/chat.json
index 9e6899ed11..1ce8f01147 100644
--- a/locales/en-US/chat.json
+++ b/locales/en-US/chat.json
@@ -32,6 +32,8 @@
"chatList.longMessageDetail": "View Details",
"clearCurrentMessages": "Clear current session messages",
"compressedHistory": "Compressed History",
+ "compression.cancel": "Uncompress",
+ "compression.cancelConfirm": "Are you sure you want to uncompress? This will restore the original messages.",
"compression.history": "History",
"compression.summary": "Summary",
"confirmClearCurrentMessages": "You are about to clear the current session messages. Once cleared, they cannot be retrieved. Please confirm your action.",
diff --git a/locales/zh-CN/chat.json b/locales/zh-CN/chat.json
index 1593abc18e..4d888b133c 100644
--- a/locales/zh-CN/chat.json
+++ b/locales/zh-CN/chat.json
@@ -32,6 +32,8 @@
"chatList.longMessageDetail": "查看详情",
"clearCurrentMessages": "清空当前会话消息",
"compressedHistory": "压缩历史",
+ "compression.cancel": "取消压缩",
+ "compression.cancelConfirm": "确定要取消压缩吗?这将恢复原始消息。",
"compression.history": "历史记录",
"compression.summary": "摘要",
"confirmClearCurrentMessages": "确认清空当前会话消息吗?清空后无法恢复",
diff --git a/packages/database/src/schemas/message.ts b/packages/database/src/schemas/message.ts
index 77c7fb404f..0d680c8d62 100644
--- a/packages/database/src/schemas/message.ts
+++ b/packages/database/src/schemas/message.ts
@@ -149,6 +149,7 @@ export const messages = pgTable(
index('messages_thread_id_idx').on(table.threadId),
index('messages_agent_id_idx').on(table.agentId),
index('messages_group_id_idx').on(table.groupId),
+ index('messages_message_group_id_idx').on(table.messageGroupId),
],
);
diff --git a/src/app/[variants]/(main)/agent/_layout/Sidebar/Header/Nav.tsx b/src/app/[variants]/(main)/agent/_layout/Sidebar/Header/Nav.tsx
index bff1a3b95f..c954f6aa1d 100644
--- a/src/app/[variants]/(main)/agent/_layout/Sidebar/Header/Nav.tsx
+++ b/src/app/[variants]/(main)/agent/_layout/Sidebar/Header/Nav.tsx
@@ -33,7 +33,7 @@ const Nav = memo(() => {
const switchTopic = useChatStore((s) => s.switchTopic);
const [openNewTopicOrSaveTopic] = useChatStore((s) => [s.openNewTopicOrSaveTopic]);
- const { mutate, isValidating } = useActionSWR('openNewTopicOrSaveTopic', openNewTopicOrSaveTopic);
+ const { mutate } = useActionSWR('openNewTopicOrSaveTopic', openNewTopicOrSaveTopic);
const handleNewTopic = () => {
// If in agent sub-route, navigate back to agent chat first
if (isProfileActive && agentId) {
@@ -46,7 +46,6 @@ const Nav = memo(() => {
diff --git a/src/features/Conversation/Messages/CompressedGroup/index.tsx b/src/features/Conversation/Messages/CompressedGroup/index.tsx
index 06a9aa3863..416ae84195 100644
--- a/src/features/Conversation/Messages/CompressedGroup/index.tsx
+++ b/src/features/Conversation/Messages/CompressedGroup/index.tsx
@@ -10,9 +10,10 @@ import {
Tabs,
type TabsProps,
} from '@lobehub/ui';
+import { App } from 'antd';
import { createStaticStyles, cx } from 'antd-style';
import isEqual from 'fast-deep-equal';
-import { ChevronDown, ChevronUp, History, Sparkles } from 'lucide-react';
+import { ChevronDown, ChevronUp, History, Sparkles, Undo2 } from 'lucide-react';
import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -66,6 +67,7 @@ export interface CompressedGroupMessageProps {
const CompressedGroupMessage = memo(({ id }) => {
const { t } = useTranslation('chat');
+ const { modal } = App.useApp();
const [activeTab, setActiveTab] = useState(() => getStoredTab(id));
const handleTabChange = useCallback(
@@ -80,6 +82,16 @@ const CompressedGroupMessage = memo(({ id }) => {
const toggleCompressedGroupExpanded = useConversationStore(
(s) => s.toggleCompressedGroupExpanded,
);
+ const cancelCompression = useConversationStore((s) => s.cancelCompression);
+
+ const handleCancelCompression = useCallback(() => {
+ modal.confirm({
+ centered: true,
+ content: t('compression.cancelConfirm'),
+ onOk: () => cancelCompression(id),
+ title: t('compression.cancel'),
+ });
+ }, [id, cancelCompression, modal, t]);
const content = message?.content;
const rawCompressedMessages = (message as UIChatMessage)?.compressedMessages;
@@ -145,11 +157,19 @@ const CompressedGroupMessage = memo(({ id }) => {
onChange={handleTabChange}
variant={'rounded'}
/>
- toggleCompressedGroupExpanded(id)}
- size={'small'}
- />
+
+
+ toggleCompressedGroupExpanded(id)}
+ size={'small'}
+ />
+
)}
{!showContent ? null : activeTab === 'summary' ? (
diff --git a/src/features/Conversation/store/slices/message/action/state.ts b/src/features/Conversation/store/slices/message/action/state.ts
index 20143a5474..289c9603ab 100644
--- a/src/features/Conversation/store/slices/message/action/state.ts
+++ b/src/features/Conversation/store/slices/message/action/state.ts
@@ -13,6 +13,11 @@ import { dataSelectors } from '../../data/selectors';
* Handles message state operations like loading, collapsed, etc.
*/
export interface MessageStateAction {
+ /**
+ * Cancel compression and restore original messages
+ */
+ cancelCompression: (id: string) => Promise;
+
/**
* Copy message content to clipboard
*/
@@ -50,6 +55,26 @@ export const messageStateSlice: StateCreator<
[],
MessageStateAction
> = (set, get) => ({
+ cancelCompression: async (id) => {
+ const message = dataSelectors.getDisplayMessageById(id)(get());
+ if (!message || message.role !== 'compressedGroup') return;
+
+ const { context, replaceMessages } = get();
+ if (!context.agentId || !context.topicId) return;
+
+ // Call service to cancel compression
+ const { messages } = await messageService.cancelCompression({
+ agentId: context.agentId,
+ groupId: context.groupId,
+ messageGroupId: id,
+ threadId: context.threadId,
+ topicId: context.topicId,
+ });
+
+ // Replace messages with restored original messages
+ replaceMessages(messages);
+ },
+
copyMessage: async (id, content) => {
const { hooks } = get();
diff --git a/src/locales/default/chat.ts b/src/locales/default/chat.ts
index 183bc108b6..40fccf486d 100644
--- a/src/locales/default/chat.ts
+++ b/src/locales/default/chat.ts
@@ -41,6 +41,9 @@ export default {
'chatList.longMessageDetail': 'View Details',
'clearCurrentMessages': 'Clear current session messages',
'compressedHistory': 'Compressed History',
+ 'compression.cancel': 'Uncompress',
+ 'compression.cancelConfirm':
+ 'Are you sure you want to uncompress? This will restore the original messages.',
'compression.history': 'History',
'compression.summary': 'Summary',
'confirmClearCurrentMessages':
diff --git a/src/server/routers/lambda/message.ts b/src/server/routers/lambda/message.ts
index b46f4c2db4..c6d7acd898 100644
--- a/src/server/routers/lambda/message.ts
+++ b/src/server/routers/lambda/message.ts
@@ -48,7 +48,32 @@ export const messageRouter = router({
return ctx.messageService.addFilesToMessage(id, fileIds, resolved);
}),
- count: messageProcedure
+ /**
+ * Cancel compression by deleting the compression group and restoring original messages
+ */
+cancelCompression: messageProcedure
+ .input(
+ z.object({
+ agentId: z.string(),
+ groupId: z.string().nullable().optional(),
+ messageGroupId: z.string(),
+ threadId: z.string().nullable().optional(),
+ topicId: z.string(),
+ }),
+ )
+ .mutation(async ({ input, ctx }) => {
+ const { messageGroupId, agentId, groupId, threadId, topicId } = input;
+
+ return ctx.messageService.cancelCompression(messageGroupId, {
+ agentId,
+ groupId,
+ threadId,
+ topicId,
+ });
+ }),
+
+
+count: messageProcedure
.input(
z
.object({
@@ -62,7 +87,9 @@ export const messageRouter = router({
return ctx.messageModel.count(input);
}),
- countWords: messageProcedure
+
+
+countWords: messageProcedure
.input(
z
.object({
@@ -76,12 +103,13 @@ export const messageRouter = router({
return ctx.messageModel.countWords(input);
}),
- /**
+
+/**
* Create a compression group for old messages
* Creates a placeholder group, marks messages as compressed
* Returns messages to summarize for frontend AI generation
*/
- createCompressionGroup: messageProcedure
+createCompressionGroup: messageProcedure
.input(
z.object({
agentId: z.string(),
@@ -102,7 +130,9 @@ export const messageRouter = router({
});
}),
- createMessage: messageProcedure
+
+
+createMessage: messageProcedure
.input(CreateNewMessageParamsSchema)
.mutation(async ({ input, ctx }) => {
// If there's no agentId but has sessionId, resolve agentId from sessionId
@@ -115,10 +145,11 @@ export const messageRouter = router({
return ctx.messageService.createMessage({ ...input, agentId } as any);
}),
+
/**
* Finalize compression by updating the group with generated summary
*/
- finalizeCompression: messageProcedure
+finalizeCompression: messageProcedure
.input(
z.object({
agentId: z.string(),
diff --git a/src/server/services/message/index.ts b/src/server/services/message/index.ts
index 8ec13b4a28..ba231e4b5b 100644
--- a/src/server/services/message/index.ts
+++ b/src/server/services/message/index.ts
@@ -359,4 +359,23 @@ export class MessageService {
return { messages };
}
+
+ /**
+ * Cancel compression by deleting the compression group and restoring original messages
+ *
+ * @param messageGroupId - The compression group ID to cancel
+ * @param context - Query options for returning updated messages
+ */
+ async cancelCompression(
+ messageGroupId: string,
+ context: QueryOptions,
+ ): Promise<{ messages: UIChatMessage[]; success: boolean }> {
+ // Delete compression group (this also unmarks messages)
+ await this.compressionRepository.deleteCompressionGroup(messageGroupId);
+
+ // Query updated messages
+ const messages = await this.messageModel.query(context, this.getQueryOptions());
+
+ return { messages, success: true };
+ }
}
diff --git a/src/services/message/index.ts b/src/services/message/index.ts
index c5ba8c0ee4..e5005df835 100644
--- a/src/services/message/index.ts
+++ b/src/services/message/index.ts
@@ -276,6 +276,20 @@ export class MessageService {
messages: (result.messages || []) as unknown as UIChatMessage[],
};
};
+
+ /**
+ * Cancel compression by deleting the compression group and restoring original messages
+ */
+ cancelCompression = async (params: {
+ agentId: string;
+ groupId?: string | null;
+ messageGroupId: string;
+ threadId?: string | null;
+ topicId: string;
+ }): Promise<{ messages: UIChatMessage[] }> => {
+ const result = await lambdaClient.message.cancelCompression.mutate(params);
+ return { messages: (result.messages || []) as unknown as UIChatMessage[] };
+ };
}
export const messageService = new MessageService();