@lobehub/lobehub 2.1.13 → 2.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/apps/desktop/src/main/menus/impls/linux.ts +1 -1
  3. package/apps/desktop/src/main/menus/impls/macOS.ts +8 -8
  4. package/apps/desktop/src/main/menus/impls/windows.ts +1 -1
  5. package/changelog/v2.json +18 -0
  6. package/docs/development/database-schema.dbml +1 -0
  7. package/locales/en-US/chat.json +2 -0
  8. package/locales/en-US/discover.json +9 -0
  9. package/locales/zh-CN/chat.json +2 -0
  10. package/locales/zh-CN/discover.json +9 -0
  11. package/package.json +1 -1
  12. package/packages/database/migrations/0076_add_message_group_index.sql +1 -0
  13. package/packages/database/migrations/meta/0076_snapshot.json +11270 -0
  14. package/packages/database/migrations/meta/_journal.json +8 -1
  15. package/packages/database/src/schemas/message.ts +1 -0
  16. package/packages/types/src/discover/assistants.ts +1 -0
  17. package/packages/types/src/discover/index.ts +8 -0
  18. package/packages/utils/src/chunkers/trimBatchProbe/trimBatchProbe.ts +1 -1
  19. package/packages/utils/src/pricing.ts +6 -6
  20. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Header/Nav.tsx +1 -2
  21. package/src/app/[variants]/(main)/community/(detail)/group_agent/features/Header.tsx +3 -3
  22. package/src/app/[variants]/(main)/community/(detail)/user/features/DetailProvider.tsx +2 -0
  23. package/src/app/[variants]/(main)/community/(detail)/user/features/StatusFilter.tsx +36 -0
  24. package/src/app/[variants]/(main)/community/(detail)/user/features/UserAgentList.tsx +69 -13
  25. package/src/app/[variants]/(main)/community/(detail)/user/features/UserContent.tsx +0 -11
  26. package/src/app/[variants]/(main)/community/(detail)/user/features/UserGroupList.tsx +68 -14
  27. package/src/app/[variants]/(main)/community/(detail)/user/index.tsx +3 -1
  28. package/src/app/[variants]/(main)/community/(list)/agent/features/List/Item.tsx +2 -1
  29. package/src/features/Conversation/Messages/CompressedGroup/index.tsx +26 -6
  30. package/src/features/Conversation/store/slices/message/action/state.ts +25 -0
  31. package/src/locales/default/chat.ts +3 -0
  32. package/src/locales/default/discover.ts +12 -0
  33. package/src/server/routers/lambda/market/social.ts +1 -1
  34. package/src/server/routers/lambda/message.ts +37 -6
  35. package/src/server/services/discover/index.ts +57 -2
  36. package/src/server/services/message/index.ts +19 -0
  37. package/src/services/message/index.ts +14 -0
  38. package/src/services/models.ts +0 -1
  39. package/src/services/social.ts +1 -1
@@ -532,7 +532,14 @@
532
532
  "when": 1769362978088,
533
533
  "tag": "0075_add_user_memory_persona",
534
534
  "breakpoints": true
535
+ },
536
+ {
537
+ "idx": 76,
538
+ "version": "7",
539
+ "when": 1770179814971,
540
+ "tag": "0076_add_message_group_index",
541
+ "breakpoints": true
535
542
  }
536
543
  ],
537
544
  "version": "6"
538
- }
545
+ }
@@ -149,6 +149,7 @@ export const messages = pgTable(
149
149
  index('messages_thread_id_idx').on(table.threadId),
150
150
  index('messages_agent_id_idx').on(table.agentId),
151
151
  index('messages_group_id_idx').on(table.groupId),
152
+ index('messages_message_group_id_idx').on(table.messageGroupId),
152
153
  ],
153
154
  );
154
155
 
@@ -62,6 +62,7 @@ export interface DiscoverAssistantItem extends Omit<LobeAgentSettings, 'meta'>,
62
62
  status?: AgentStatus;
63
63
  tokenUsage: number;
64
64
  type?: AgentType;
65
+ updatedAt?: string;
65
66
  userName?: string;
66
67
  }
67
68
 
@@ -67,6 +67,14 @@ export interface DiscoverUserInfo {
67
67
  export interface DiscoverUserProfile {
68
68
  agentGroups?: DiscoverGroupAgentItem[];
69
69
  agents: DiscoverAssistantItem[];
70
+ /**
71
+ * Agent groups favorited by the user
72
+ */
73
+ favoriteAgentGroups?: DiscoverGroupAgentItem[];
74
+ /**
75
+ * Agents favorited by the user
76
+ */
77
+ favoriteAgents?: DiscoverAssistantItem[];
70
78
  /**
71
79
  * Agent groups forked by the user
72
80
  */
@@ -202,7 +202,7 @@ export const handleSingle = async (
202
202
  * to avoid breaking structured XML/JSON-like content mid-way.
203
203
  *
204
204
  * Bisection example (8 segments, keep newest):
205
- * try 4 (fits?) yes try 6 no try 5 yes best=5
205
+ * try 4 (fits?) -> yes -> try 6 -> no -> try 5 -> yes => best=5
206
206
  * if compact retry needed, repeat with build(true) and pick the better fit.
207
207
  *
208
208
  * This minimizes structural breakage by preferring whole built segments and only truncating the last one as a last resort.
@@ -2,9 +2,9 @@ import { Pricing, PricingUnit, PricingUnitName } from 'model-bank';
2
2
 
3
3
  /**
4
4
  * Internal helper to extract the displayed unit rate from a pricing unit by strategy
5
- * - fixed rate
6
- * - tiered tiers[0].rate
7
- * - lookup first price value
5
+ * - fixed: rate
6
+ * - tiered: tiers[0].rate
7
+ * - lookup: first price value
8
8
  */
9
9
  const getRateFromUnit = (unit: PricingUnit): number | undefined => {
10
10
  switch (unit.strategy) {
@@ -39,9 +39,9 @@ export const getUnitRateByName = (
39
39
 
40
40
  /**
41
41
  * Get text input unit rate from pricing
42
- * - fixed rate
43
- * - tiered tiers[0].rate
44
- * - lookup Object.values(lookup.prices)[0]
42
+ * - fixed: rate
43
+ * - tiered: tiers[0].rate
44
+ * - lookup: Object.values(lookup.prices)[0]
45
45
  */
46
46
  export function getTextInputUnitRate(pricing?: Pricing): number | undefined {
47
47
  return getUnitRateByName(pricing, 'textInput');
@@ -33,7 +33,7 @@ const Nav = memo(() => {
33
33
  const switchTopic = useChatStore((s) => s.switchTopic);
34
34
  const [openNewTopicOrSaveTopic] = useChatStore((s) => [s.openNewTopicOrSaveTopic]);
35
35
 
36
- const { mutate, isValidating } = useActionSWR('openNewTopicOrSaveTopic', openNewTopicOrSaveTopic);
36
+ const { mutate } = useActionSWR('openNewTopicOrSaveTopic', openNewTopicOrSaveTopic);
37
37
  const handleNewTopic = () => {
38
38
  // If in agent sub-route, navigate back to agent chat first
39
39
  if (isProfileActive && agentId) {
@@ -46,7 +46,6 @@ const Nav = memo(() => {
46
46
  <Flexbox gap={1} paddingInline={4}>
47
47
  <NavItem
48
48
  icon={MessageSquarePlusIcon}
49
- loading={isValidating}
50
49
  onClick={handleNewTopic}
51
50
  title={tTopic('actions.addNewTopic')}
52
51
  />
@@ -75,7 +75,7 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
75
75
  // Fetch favorite status
76
76
  const { data: favoriteStatus, mutate: mutateFavorite } = useSWR(
77
77
  identifier && isAuthenticated ? ['favorite-status', 'agent', identifier] : null,
78
- () => socialService.checkFavoriteStatus('agent', identifier!),
78
+ () => socialService.checkFavoriteStatus('agent-group', identifier!),
79
79
  { revalidateOnFocus: false },
80
80
  );
81
81
 
@@ -100,10 +100,10 @@ const Header = memo<{ mobile?: boolean }>(({ mobile: isMobile }) => {
100
100
  setFavoriteLoading(true);
101
101
  try {
102
102
  if (isFavorited) {
103
- await socialService.removeFavorite('agent', identifier);
103
+ await socialService.removeFavorite('agent-group', identifier);
104
104
  message.success(t('assistant.unfavoriteSuccess'));
105
105
  } else {
106
- await socialService.addFavorite('agent', identifier);
106
+ await socialService.addFavorite('agent-group', identifier);
107
107
  message.success(t('assistant.favoriteSuccess'));
108
108
  }
109
109
  await mutateFavorite();
@@ -13,6 +13,8 @@ export interface UserDetailContextConfig {
13
13
  agentCount: number;
14
14
  agentGroups?: DiscoverGroupAgentItem[];
15
15
  agents: DiscoverAssistantItem[];
16
+ favoriteAgentGroups?: DiscoverGroupAgentItem[];
17
+ favoriteAgents?: DiscoverAssistantItem[];
16
18
  forkedAgentGroups?: DiscoverGroupAgentItem[];
17
19
  forkedAgents?: DiscoverAssistantItem[];
18
20
  groupCount: number;
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+
3
+ import { Select } from 'antd';
4
+ import { memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ export type StatusFilterValue = 'published' | 'unpublished' | 'deprecated' | 'archived' | 'forked' | 'favorite';
8
+
9
+ interface StatusFilterProps {
10
+ onChange: (value: StatusFilterValue) => void;
11
+ value: StatusFilterValue;
12
+ }
13
+
14
+ const StatusFilter = memo<StatusFilterProps>(({ value, onChange }) => {
15
+ const { t } = useTranslation('discover');
16
+
17
+ const options = [
18
+ { label: t('user.statusFilter.published'), value: 'published' as const },
19
+ { label: t('user.statusFilter.unpublished'), value: 'unpublished' as const },
20
+ { label: t('user.statusFilter.deprecated'), value: 'deprecated' as const },
21
+ { label: t('user.statusFilter.archived'), value: 'archived' as const },
22
+ { label: t('user.statusFilter.forked'), value: 'forked' as const },
23
+ { label: t('user.statusFilter.favorite'), value: 'favorite' as const },
24
+ ];
25
+
26
+ return (
27
+ <Select
28
+ onChange={onChange}
29
+ options={options}
30
+ style={{ minWidth: 120 }}
31
+ value={value}
32
+ />
33
+ );
34
+ });
35
+
36
+ export default StatusFilter;
@@ -1,12 +1,13 @@
1
1
  'use client';
2
2
 
3
3
  import { Flexbox, Grid, Tag, Text } from '@lobehub/ui';
4
- import { Pagination } from 'antd';
4
+ import { Input, Pagination } from 'antd';
5
5
  import { memo, useMemo, useState } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
 
8
8
  import AssistantEmpty from '../../../features/AssistantEmpty';
9
9
  import { useUserDetailContext } from './DetailProvider';
10
+ import StatusFilter, { type StatusFilterValue } from './StatusFilter';
10
11
  import UserAgentCard from './UserAgentCard';
11
12
 
12
13
  interface UserAgentListProps {
@@ -14,27 +15,82 @@ interface UserAgentListProps {
14
15
  rows?: number;
15
16
  }
16
17
 
17
- const UserAgentList = memo<UserAgentListProps>(({ rows = 4, pageSize = 10 }) => {
18
+ const UserAgentList = memo<UserAgentListProps>(({ rows = 4, pageSize = 8 }) => {
18
19
  const { t } = useTranslation('discover');
19
- const { agents, agentCount } = useUserDetailContext();
20
+ const { agents, agentCount, forkedAgents = [], favoriteAgents = [], isOwner } = useUserDetailContext();
20
21
  const [currentPage, setCurrentPage] = useState(1);
22
+ const [statusFilter, setStatusFilter] = useState<StatusFilterValue>('published');
23
+ const [searchQuery, setSearchQuery] = useState('');
24
+
25
+ // Combine agents and forked agents, then filter based on status and search
26
+ const filteredAgents = useMemo(() => {
27
+ let allAgents = [...agents];
28
+
29
+ if (statusFilter === 'forked') {
30
+ // Show only forked agents (those with forkedFromAgentId)
31
+ allAgents = forkedAgents;
32
+ } else if (statusFilter === 'favorite') {
33
+ // Show only favorited agents
34
+ allAgents = favoriteAgents;
35
+ } else {
36
+ // Filter by status for non-forked agents
37
+ allAgents = allAgents.filter((agent) => {
38
+ return agent.status === statusFilter;
39
+ });
40
+ }
41
+
42
+ // Apply search filter
43
+ if (searchQuery.trim()) {
44
+ const query = searchQuery.toLowerCase();
45
+ allAgents = allAgents.filter((agent) => {
46
+ console.log('agent', agent);
47
+ const name = agent?.title?.toLowerCase() || '';
48
+ const description = agent?.description?.toLowerCase() || '';
49
+ return name.includes(query) || description.includes(query);
50
+ });
51
+ }
52
+
53
+ return allAgents;
54
+ }, [agents, forkedAgents, statusFilter, searchQuery]);
21
55
 
22
56
  const paginatedAgents = useMemo(() => {
23
57
  const startIndex = (currentPage - 1) * pageSize;
24
- return agents.slice(startIndex, startIndex + pageSize);
25
- }, [agents, currentPage, pageSize]);
58
+ return filteredAgents.slice(startIndex, startIndex + pageSize);
59
+ }, [filteredAgents, currentPage, pageSize]);
60
+
61
+ // Reset to page 1 when filter or search changes
62
+ useMemo(() => {
63
+ setCurrentPage(1);
64
+ }, [statusFilter, searchQuery]);
26
65
 
27
- if (agents.length === 0) return <AssistantEmpty />;
66
+ if (agents.length === 0 && forkedAgents.length === 0) return <AssistantEmpty />;
28
67
 
29
- const showPagination = agents.length > pageSize;
68
+ const showPagination = filteredAgents.length > pageSize;
30
69
 
31
70
  return (
32
71
  <Flexbox gap={16}>
33
- <Flexbox align={'center'} gap={8} horizontal>
34
- <Text fontSize={16} weight={500}>
35
- {t('user.publishedAgents')}
36
- </Text>
37
- {agentCount > 0 && <Tag>{agentCount}</Tag>}
72
+ <Flexbox align={'center'} gap={8} horizontal justify={'space-between'}>
73
+ <Flexbox align={'center'} gap={8} horizontal>
74
+ <Text fontSize={16} weight={500}>
75
+ {t('user.publishedAgents')}
76
+ </Text>
77
+ {agentCount > 0 && <Tag>{filteredAgents.length}</Tag>}
78
+ </Flexbox>
79
+ {isOwner && (
80
+ <Flexbox align={'center'} gap={8} horizontal>
81
+ <Input.Search
82
+ allowClear
83
+ onChange={(e) => setSearchQuery(e.target.value)}
84
+ placeholder={t('user.searchPlaceholder')}
85
+ style={{ width: 200 }}
86
+ value={searchQuery}
87
+ />
88
+ <StatusFilter
89
+ onChange={(value) => setStatusFilter(value)}
90
+ value={statusFilter}
91
+ />
92
+ </Flexbox>
93
+ )}
38
94
  </Flexbox>
39
95
  <Grid rows={rows} width={'100%'}>
40
96
  {paginatedAgents.map((item, index) => (
@@ -48,7 +104,7 @@ const UserAgentList = memo<UserAgentListProps>(({ rows = 4, pageSize = 10 }) =>
48
104
  onChange={(page) => setCurrentPage(page)}
49
105
  pageSize={pageSize}
50
106
  showSizeChanger={false}
51
- total={agents.length}
107
+ total={filteredAgents.length}
52
108
  />
53
109
  </Flexbox>
54
110
  )}
@@ -3,25 +3,14 @@
3
3
  import { Flexbox } from '@lobehub/ui';
4
4
  import { memo } from 'react';
5
5
 
6
- import { useUserDetailContext } from './DetailProvider';
7
6
  import UserAgentList from './UserAgentList';
8
- import UserFavoriteAgents from './UserFavoriteAgents';
9
- import UserFavoritePlugins from './UserFavoritePlugins';
10
- import UserForkedAgentGroups from './UserForkedAgentGroups';
11
- import UserForkedAgents from './UserForkedAgents';
12
7
  import UserGroupList from './UserGroupList';
13
8
 
14
9
  const UserContent = memo(() => {
15
- const { forkedAgents, forkedAgentGroups } = useUserDetailContext();
16
-
17
10
  return (
18
11
  <Flexbox gap={32}>
19
12
  <UserAgentList />
20
13
  <UserGroupList />
21
- <UserForkedAgents agents={forkedAgents} />
22
- <UserForkedAgentGroups agentGroups={forkedAgentGroups} />
23
- <UserFavoriteAgents />
24
- <UserFavoritePlugins />
25
14
  </Flexbox>
26
15
  );
27
16
  });
@@ -1,11 +1,12 @@
1
1
  'use client';
2
2
 
3
3
  import { Flexbox, Grid, Tag, Text } from '@lobehub/ui';
4
- import { Pagination } from 'antd';
4
+ import { Input, Pagination } from 'antd';
5
5
  import { memo, useMemo, useState } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
 
8
8
  import { useUserDetailContext } from './DetailProvider';
9
+ import StatusFilter, { type StatusFilterValue } from './StatusFilter';
9
10
  import UserGroupCard from './UserGroupCard';
10
11
 
11
12
  interface UserGroupListProps {
@@ -13,28 +14,81 @@ interface UserGroupListProps {
13
14
  rows?: number;
14
15
  }
15
16
 
16
- const UserGroupList = memo<UserGroupListProps>(({ rows = 4, pageSize = 10 }) => {
17
+ const UserGroupList = memo<UserGroupListProps>(({ rows = 4, pageSize = 8 }) => {
17
18
  const { t } = useTranslation('discover');
18
- const { agentGroups, groupCount } = useUserDetailContext();
19
+ const { agentGroups = [], groupCount, forkedAgentGroups = [], favoriteAgentGroups = [], isOwner } = useUserDetailContext();
19
20
  const [currentPage, setCurrentPage] = useState(1);
21
+ const [statusFilter, setStatusFilter] = useState<StatusFilterValue>('published');
22
+ const [searchQuery, setSearchQuery] = useState('');
23
+
24
+ // Combine groups and forked groups, then filter based on status and search
25
+ const filteredGroups = useMemo(() => {
26
+ let allGroups = [...agentGroups];
27
+
28
+ if (statusFilter === 'forked') {
29
+ // Show only forked groups (those with forkedFromAgentId)
30
+ allGroups = forkedAgentGroups;
31
+ } else if (statusFilter === 'favorite') {
32
+ // Show only favorited groups
33
+ allGroups = favoriteAgentGroups;
34
+ } else {
35
+ // Filter by status for non-forked groups
36
+ allGroups = allGroups.filter((group) => {
37
+ return group.status === statusFilter;
38
+ });
39
+ }
40
+
41
+ // Apply search filter
42
+ if (searchQuery.trim()) {
43
+ const query = searchQuery.toLowerCase();
44
+ allGroups = allGroups.filter((group) => {
45
+ const name = group?.title?.toLowerCase() || '';
46
+ const description = group?.description?.toLowerCase() || '';
47
+ return name.includes(query) || description.includes(query);
48
+ });
49
+ }
50
+
51
+ return allGroups;
52
+ }, [agentGroups, forkedAgentGroups, statusFilter, searchQuery]);
20
53
 
21
54
  const paginatedGroups = useMemo(() => {
22
- if (!agentGroups) return [];
23
55
  const startIndex = (currentPage - 1) * pageSize;
24
- return agentGroups.slice(startIndex, startIndex + pageSize);
25
- }, [agentGroups, currentPage, pageSize]);
56
+ return filteredGroups.slice(startIndex, startIndex + pageSize);
57
+ }, [filteredGroups, currentPage, pageSize]);
58
+
59
+ // Reset to page 1 when filter or search changes
60
+ useMemo(() => {
61
+ setCurrentPage(1);
62
+ }, [statusFilter, searchQuery]);
26
63
 
27
- if (!agentGroups || agentGroups.length === 0) return null;
64
+ if (agentGroups.length === 0 && forkedAgentGroups.length === 0) return null;
28
65
 
29
- const showPagination = agentGroups.length > pageSize;
66
+ const showPagination = filteredGroups.length > pageSize;
30
67
 
31
68
  return (
32
69
  <Flexbox gap={16}>
33
- <Flexbox align={'center'} gap={8} horizontal>
34
- <Text fontSize={16} weight={500}>
35
- {t('user.publishedGroups', { defaultValue: '创作的群组' })}
36
- </Text>
37
- {groupCount > 0 && <Tag>{groupCount}</Tag>}
70
+ <Flexbox align={'center'} gap={8} horizontal justify={'space-between'}>
71
+ <Flexbox align={'center'} gap={8} horizontal>
72
+ <Text fontSize={16} weight={500}>
73
+ {t('user.publishedGroups', { defaultValue: '创作的群组' })}
74
+ </Text>
75
+ {groupCount > 0 && <Tag>{filteredGroups.length}</Tag>}
76
+ </Flexbox>
77
+ {isOwner && (
78
+ <Flexbox align={'center'} gap={8} horizontal>
79
+ <Input.Search
80
+ allowClear
81
+ onChange={(e) => setSearchQuery(e.target.value)}
82
+ placeholder={t('user.searchPlaceholder')}
83
+ style={{ width: 200 }}
84
+ value={searchQuery}
85
+ />
86
+ <StatusFilter
87
+ onChange={(value) => setStatusFilter(value)}
88
+ value={statusFilter}
89
+ />
90
+ </Flexbox>
91
+ )}
38
92
  </Flexbox>
39
93
  <Grid rows={rows} width={'100%'}>
40
94
  {paginatedGroups.map((item, index) => (
@@ -48,7 +102,7 @@ const UserGroupList = memo<UserGroupListProps>(({ rows = 4, pageSize = 10 }) =>
48
102
  onChange={(page) => setCurrentPage(page)}
49
103
  pageSize={pageSize}
50
104
  showSizeChanger={false}
51
- total={agentGroups.length}
105
+ total={filteredGroups.length}
52
106
  />
53
107
  </Flexbox>
54
108
  )}
@@ -61,12 +61,14 @@ const UserDetailPage = memo<UserDetailPageProps>(({ mobile }) => {
61
61
 
62
62
  const contextConfig = useMemo(() => {
63
63
  if (!data || !data.user) return null;
64
- const { user, agents, agentGroups, forkedAgents, forkedAgentGroups } = data;
64
+ const { user, agents, agentGroups, forkedAgents, forkedAgentGroups, favoriteAgents, favoriteAgentGroups } = data;
65
65
  const totalInstalls = agents.reduce((sum, agent) => sum + (agent.installCount || 0), 0);
66
66
  return {
67
67
  agentCount: agents.length,
68
68
  agentGroups: agentGroups || [],
69
69
  agents,
70
+ favoriteAgentGroups: favoriteAgentGroups || [],
71
+ favoriteAgents: favoriteAgents || [],
70
72
  forkedAgentGroups: forkedAgentGroups || [],
71
73
  forkedAgents: forkedAgents || [],
72
74
  groupCount: agentGroups?.length || 0,
@@ -56,6 +56,7 @@ const styles = createStaticStyles(({ css, cssVar }) => {
56
56
  const AssistantItem = memo<DiscoverAssistantItem>(
57
57
  ({
58
58
  createdAt,
59
+ updatedAt,
59
60
  author,
60
61
  avatar,
61
62
  title,
@@ -225,7 +226,7 @@ const AssistantItem = memo<DiscoverAssistantItem>(
225
226
  <Icon icon={ClockIcon} size={14} />
226
227
  <PublishedTime
227
228
  className={styles.secondaryDesc}
228
- date={createdAt}
229
+ date={updatedAt || createdAt}
229
230
  template={'MMM DD, YYYY'}
230
231
  />
231
232
  </Flexbox>
@@ -10,9 +10,10 @@ import {
10
10
  Tabs,
11
11
  type TabsProps,
12
12
  } from '@lobehub/ui';
13
+ import { App } from 'antd';
13
14
  import { createStaticStyles, cx } from 'antd-style';
14
15
  import isEqual from 'fast-deep-equal';
15
- import { ChevronDown, ChevronUp, History, Sparkles } from 'lucide-react';
16
+ import { ChevronDown, ChevronUp, History, Sparkles, Undo2 } from 'lucide-react';
16
17
  import { memo, useCallback, useMemo, useState } from 'react';
17
18
  import { useTranslation } from 'react-i18next';
18
19
 
@@ -66,6 +67,7 @@ export interface CompressedGroupMessageProps {
66
67
 
67
68
  const CompressedGroupMessage = memo<CompressedGroupMessageProps>(({ id }) => {
68
69
  const { t } = useTranslation('chat');
70
+ const { modal } = App.useApp();
69
71
  const [activeTab, setActiveTab] = useState<string>(() => getStoredTab(id));
70
72
 
71
73
  const handleTabChange = useCallback(
@@ -80,6 +82,16 @@ const CompressedGroupMessage = memo<CompressedGroupMessageProps>(({ id }) => {
80
82
  const toggleCompressedGroupExpanded = useConversationStore(
81
83
  (s) => s.toggleCompressedGroupExpanded,
82
84
  );
85
+ const cancelCompression = useConversationStore((s) => s.cancelCompression);
86
+
87
+ const handleCancelCompression = useCallback(() => {
88
+ modal.confirm({
89
+ centered: true,
90
+ content: t('compression.cancelConfirm'),
91
+ onOk: () => cancelCompression(id),
92
+ title: t('compression.cancel'),
93
+ });
94
+ }, [id, cancelCompression, modal, t]);
83
95
 
84
96
  const content = message?.content;
85
97
  const rawCompressedMessages = (message as UIChatMessage)?.compressedMessages;
@@ -145,11 +157,19 @@ const CompressedGroupMessage = memo<CompressedGroupMessageProps>(({ id }) => {
145
157
  onChange={handleTabChange}
146
158
  variant={'rounded'}
147
159
  />
148
- <ActionIcon
149
- icon={expanded ? ChevronUp : ChevronDown}
150
- onClick={() => toggleCompressedGroupExpanded(id)}
151
- size={'small'}
152
- />
160
+ <Flexbox gap={4} horizontal>
161
+ <ActionIcon
162
+ icon={Undo2}
163
+ onClick={handleCancelCompression}
164
+ size={'small'}
165
+ title={t('compression.cancel')}
166
+ />
167
+ <ActionIcon
168
+ icon={expanded ? ChevronUp : ChevronDown}
169
+ onClick={() => toggleCompressedGroupExpanded(id)}
170
+ size={'small'}
171
+ />
172
+ </Flexbox>
153
173
  </Flexbox>
154
174
  )}
155
175
  {!showContent ? null : activeTab === 'summary' ? (
@@ -13,6 +13,11 @@ import { dataSelectors } from '../../data/selectors';
13
13
  * Handles message state operations like loading, collapsed, etc.
14
14
  */
15
15
  export interface MessageStateAction {
16
+ /**
17
+ * Cancel compression and restore original messages
18
+ */
19
+ cancelCompression: (id: string) => Promise<void>;
20
+
16
21
  /**
17
22
  * Copy message content to clipboard
18
23
  */
@@ -50,6 +55,26 @@ export const messageStateSlice: StateCreator<
50
55
  [],
51
56
  MessageStateAction
52
57
  > = (set, get) => ({
58
+ cancelCompression: async (id) => {
59
+ const message = dataSelectors.getDisplayMessageById(id)(get());
60
+ if (!message || message.role !== 'compressedGroup') return;
61
+
62
+ const { context, replaceMessages } = get();
63
+ if (!context.agentId || !context.topicId) return;
64
+
65
+ // Call service to cancel compression
66
+ const { messages } = await messageService.cancelCompression({
67
+ agentId: context.agentId,
68
+ groupId: context.groupId,
69
+ messageGroupId: id,
70
+ threadId: context.threadId,
71
+ topicId: context.topicId,
72
+ });
73
+
74
+ // Replace messages with restored original messages
75
+ replaceMessages(messages);
76
+ },
77
+
53
78
  copyMessage: async (id, content) => {
54
79
  const { hooks } = get();
55
80
 
@@ -41,6 +41,9 @@ export default {
41
41
  'chatList.longMessageDetail': 'View Details',
42
42
  'clearCurrentMessages': 'Clear current session messages',
43
43
  'compressedHistory': 'Compressed History',
44
+ 'compression.cancel': 'Uncompress',
45
+ 'compression.cancelConfirm':
46
+ 'Are you sure you want to uncompress? This will restore the original messages.',
44
47
  'compression.history': 'History',
45
48
  'compression.summary': 'Summary',
46
49
  'confirmClearCurrentMessages':
@@ -891,6 +891,18 @@ export default {
891
891
 
892
892
  'user.publishedAgents': 'Created Agents',
893
893
 
894
+ 'user.publishedGroups': 'Created Groups',
895
+
896
+ 'user.searchPlaceholder': 'Search by name or description...',
897
+
898
+ 'user.statusFilter.all': 'All',
899
+ 'user.statusFilter.archived': 'Archived',
900
+ 'user.statusFilter.deprecated': 'Deprecated',
901
+ 'user.statusFilter.favorite': 'Favorite',
902
+ 'user.statusFilter.forked': 'Forked',
903
+ 'user.statusFilter.published': 'Published',
904
+ 'user.statusFilter.unpublished': 'Under Review',
905
+
894
906
  'user.tabs.favorites': 'Favorites',
895
907
 
896
908
  'user.tabs.forkedAgents': 'Forked',
@@ -17,7 +17,7 @@ const socialPublicProcedure = publicProcedure
17
17
  .use(marketSDK);
18
18
 
19
19
  // Schema definitions
20
- const targetTypeSchema = z.enum(['agent', 'plugin']);
20
+ const targetTypeSchema = z.enum(['agent', 'plugin', 'agent-group']);
21
21
 
22
22
  const paginationSchema = z.object({
23
23
  limit: z.number().optional(),