@lobehub/lobehub 2.0.0-next.264 → 2.0.0-next.266

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 (157) hide show
  1. package/.github/workflows/manual-build-desktop.yml +16 -37
  2. package/CHANGELOG.md +52 -0
  3. package/apps/desktop/native-deps.config.mjs +19 -3
  4. package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +13 -0
  5. package/apps/desktop/src/main/utils/permissions.ts +86 -22
  6. package/changelog/v1.json +18 -0
  7. package/locales/ar/chat.json +1 -0
  8. package/locales/ar/modelProvider.json +20 -0
  9. package/locales/ar/models.json +33 -10
  10. package/locales/ar/plugin.json +1 -0
  11. package/locales/ar/providers.json +1 -0
  12. package/locales/ar/setting.json +2 -0
  13. package/locales/bg-BG/chat.json +1 -0
  14. package/locales/bg-BG/modelProvider.json +20 -0
  15. package/locales/bg-BG/models.json +27 -7
  16. package/locales/bg-BG/plugin.json +1 -0
  17. package/locales/bg-BG/providers.json +1 -0
  18. package/locales/bg-BG/setting.json +2 -0
  19. package/locales/de-DE/chat.json +1 -0
  20. package/locales/de-DE/modelProvider.json +20 -0
  21. package/locales/de-DE/models.json +44 -10
  22. package/locales/de-DE/plugin.json +1 -0
  23. package/locales/de-DE/providers.json +1 -0
  24. package/locales/de-DE/setting.json +2 -0
  25. package/locales/en-US/chat.json +1 -0
  26. package/locales/en-US/modelProvider.json +20 -0
  27. package/locales/en-US/models.json +10 -10
  28. package/locales/en-US/providers.json +1 -0
  29. package/locales/en-US/setting.json +2 -1
  30. package/locales/es-ES/chat.json +1 -0
  31. package/locales/es-ES/modelProvider.json +20 -0
  32. package/locales/es-ES/models.json +53 -10
  33. package/locales/es-ES/plugin.json +1 -0
  34. package/locales/es-ES/providers.json +1 -0
  35. package/locales/es-ES/setting.json +2 -0
  36. package/locales/fa-IR/chat.json +1 -0
  37. package/locales/fa-IR/modelProvider.json +20 -0
  38. package/locales/fa-IR/models.json +33 -10
  39. package/locales/fa-IR/plugin.json +1 -0
  40. package/locales/fa-IR/providers.json +1 -0
  41. package/locales/fa-IR/setting.json +2 -0
  42. package/locales/fr-FR/chat.json +1 -0
  43. package/locales/fr-FR/modelProvider.json +20 -0
  44. package/locales/fr-FR/models.json +27 -7
  45. package/locales/fr-FR/plugin.json +1 -0
  46. package/locales/fr-FR/providers.json +1 -0
  47. package/locales/fr-FR/setting.json +2 -0
  48. package/locales/it-IT/chat.json +1 -0
  49. package/locales/it-IT/modelProvider.json +20 -0
  50. package/locales/it-IT/models.json +10 -10
  51. package/locales/it-IT/plugin.json +1 -0
  52. package/locales/it-IT/providers.json +1 -0
  53. package/locales/it-IT/setting.json +2 -0
  54. package/locales/ja-JP/chat.json +1 -0
  55. package/locales/ja-JP/modelProvider.json +20 -0
  56. package/locales/ja-JP/models.json +5 -10
  57. package/locales/ja-JP/plugin.json +1 -0
  58. package/locales/ja-JP/providers.json +1 -0
  59. package/locales/ja-JP/setting.json +2 -0
  60. package/locales/ko-KR/chat.json +1 -0
  61. package/locales/ko-KR/modelProvider.json +20 -0
  62. package/locales/ko-KR/models.json +36 -10
  63. package/locales/ko-KR/plugin.json +1 -0
  64. package/locales/ko-KR/providers.json +1 -0
  65. package/locales/ko-KR/setting.json +2 -0
  66. package/locales/nl-NL/chat.json +1 -0
  67. package/locales/nl-NL/modelProvider.json +20 -0
  68. package/locales/nl-NL/models.json +35 -4
  69. package/locales/nl-NL/plugin.json +1 -0
  70. package/locales/nl-NL/providers.json +1 -0
  71. package/locales/nl-NL/setting.json +2 -0
  72. package/locales/pl-PL/chat.json +1 -0
  73. package/locales/pl-PL/modelProvider.json +20 -0
  74. package/locales/pl-PL/models.json +37 -7
  75. package/locales/pl-PL/plugin.json +1 -0
  76. package/locales/pl-PL/providers.json +1 -0
  77. package/locales/pl-PL/setting.json +2 -0
  78. package/locales/pt-BR/chat.json +1 -0
  79. package/locales/pt-BR/modelProvider.json +20 -0
  80. package/locales/pt-BR/models.json +51 -9
  81. package/locales/pt-BR/plugin.json +1 -0
  82. package/locales/pt-BR/providers.json +1 -0
  83. package/locales/pt-BR/setting.json +2 -0
  84. package/locales/ru-RU/chat.json +1 -0
  85. package/locales/ru-RU/modelProvider.json +20 -0
  86. package/locales/ru-RU/models.json +48 -7
  87. package/locales/ru-RU/plugin.json +1 -0
  88. package/locales/ru-RU/providers.json +1 -0
  89. package/locales/ru-RU/setting.json +2 -0
  90. package/locales/tr-TR/chat.json +1 -0
  91. package/locales/tr-TR/modelProvider.json +20 -0
  92. package/locales/tr-TR/models.json +48 -7
  93. package/locales/tr-TR/plugin.json +1 -0
  94. package/locales/tr-TR/providers.json +1 -0
  95. package/locales/tr-TR/setting.json +2 -0
  96. package/locales/vi-VN/chat.json +1 -0
  97. package/locales/vi-VN/modelProvider.json +20 -0
  98. package/locales/vi-VN/models.json +5 -5
  99. package/locales/vi-VN/plugin.json +1 -0
  100. package/locales/vi-VN/providers.json +1 -0
  101. package/locales/vi-VN/setting.json +2 -0
  102. package/locales/zh-CN/modelProvider.json +20 -20
  103. package/locales/zh-CN/models.json +49 -8
  104. package/locales/zh-CN/providers.json +1 -0
  105. package/locales/zh-CN/setting.json +2 -1
  106. package/locales/zh-TW/chat.json +1 -0
  107. package/locales/zh-TW/modelProvider.json +20 -0
  108. package/locales/zh-TW/models.json +29 -10
  109. package/locales/zh-TW/plugin.json +1 -0
  110. package/locales/zh-TW/providers.json +1 -0
  111. package/locales/zh-TW/setting.json +2 -0
  112. package/package.json +2 -2
  113. package/packages/database/src/models/__tests__/agent.test.ts +165 -4
  114. package/packages/database/src/models/agent.ts +46 -0
  115. package/packages/database/src/repositories/agentGroup/index.test.ts +498 -0
  116. package/packages/database/src/repositories/agentGroup/index.ts +150 -0
  117. package/packages/database/src/repositories/home/__tests__/index.test.ts +113 -1
  118. package/packages/database/src/repositories/home/index.ts +48 -67
  119. package/pnpm-workspace.yaml +1 -0
  120. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Body.tsx +1 -1
  121. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Cron/CronTopicGroup.tsx +84 -0
  122. package/src/app/[variants]/(main)/agent/_layout/Sidebar/{Topic/CronTopicList → Cron}/CronTopicItem.tsx +1 -1
  123. package/src/app/[variants]/(main)/agent/_layout/Sidebar/{Topic/CronTopicList → Cron}/index.tsx +23 -33
  124. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/List/Item/Editing.tsx +12 -49
  125. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/List/index.tsx +3 -1
  126. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/TopicListContent/ThreadList/ThreadItem/Editing.tsx +12 -40
  127. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/hooks/useTopicNavigation.ts +5 -1
  128. package/src/app/[variants]/(main)/agent/features/Conversation/MainChatInput/index.tsx +2 -2
  129. package/src/app/[variants]/(main)/agent/profile/features/AgentCronJobs/CronJobCards.tsx +1 -1
  130. package/src/app/[variants]/(main)/agent/profile/features/AgentCronJobs/CronJobForm.tsx +1 -1
  131. package/src/app/[variants]/(main)/group/_layout/Sidebar/AddGroupMemberModal/AvailableAgentList.tsx +0 -1
  132. package/src/app/[variants]/(main)/group/_layout/Sidebar/AddGroupMemberModal/index.tsx +5 -1
  133. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/index.tsx +2 -6
  134. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/useDropdownMenu.tsx +100 -0
  135. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/index.tsx +2 -4
  136. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/useDropdownMenu.tsx +149 -0
  137. package/src/app/[variants]/(main)/home/_layout/hooks/index.ts +0 -1
  138. package/src/app/[variants]/(main)/home/features/InputArea/index.tsx +1 -1
  139. package/src/components/InlineRename/index.tsx +121 -0
  140. package/src/features/ChatInput/InputEditor/index.tsx +1 -0
  141. package/src/features/EditorCanvas/DiffAllToolbar.tsx +1 -1
  142. package/src/features/NavPanel/components/NavItem.tsx +1 -1
  143. package/src/locales/default/setting.ts +2 -0
  144. package/src/server/routers/lambda/agent.ts +15 -0
  145. package/src/server/routers/lambda/agentGroup.ts +16 -0
  146. package/src/services/agent.ts +11 -0
  147. package/src/services/chatGroup/index.ts +11 -0
  148. package/src/store/agent/slices/cron/action.ts +108 -0
  149. package/src/store/agent/slices/cron/index.ts +1 -0
  150. package/src/store/agent/store.ts +3 -0
  151. package/src/store/home/slices/sidebarUI/action.test.ts +23 -22
  152. package/src/store/home/slices/sidebarUI/action.ts +37 -9
  153. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/CronTopicList/CronTopicGroup.tsx +0 -74
  154. package/src/app/[variants]/(main)/group/features/ChangelogModal.tsx +0 -11
  155. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Item/useDropdownMenu.tsx +0 -62
  156. package/src/app/[variants]/(main)/home/_layout/hooks/useSessionItemMenuItems.tsx +0 -238
  157. package/src/hooks/useFetchCronTopicsWithJobInfo.ts +0 -56
@@ -36,6 +36,86 @@ describe('HomeRepository', () => {
36
36
  expect(result.groups).toEqual([]);
37
37
  });
38
38
 
39
+ it('should return non-virtual agents without agentsToSessions relationship', async () => {
40
+ // Create an agent without session relationship (e.g., duplicated agent)
41
+ const agentId = 'standalone-agent';
42
+
43
+ await clientDB.insert(Schema.agents).values({
44
+ id: agentId,
45
+ userId,
46
+ title: 'Standalone Agent',
47
+ description: 'Agent without session',
48
+ pinned: false,
49
+ virtual: false,
50
+ });
51
+
52
+ const result = await homeRepo.getSidebarAgentList();
53
+
54
+ // Agent should appear in ungrouped list even without agentsToSessions
55
+ expect(result.ungrouped).toHaveLength(1);
56
+ expect(result.ungrouped[0].id).toBe(agentId);
57
+ expect(result.ungrouped[0].title).toBe('Standalone Agent');
58
+ });
59
+
60
+ it('should return pinned non-virtual agents without agentsToSessions relationship', async () => {
61
+ // Create a pinned agent without session relationship
62
+ const agentId = 'pinned-standalone';
63
+
64
+ await clientDB.insert(Schema.agents).values({
65
+ id: agentId,
66
+ userId,
67
+ title: 'Pinned Standalone Agent',
68
+ pinned: true,
69
+ virtual: false,
70
+ });
71
+
72
+ const result = await homeRepo.getSidebarAgentList();
73
+
74
+ // Agent should appear in pinned list
75
+ expect(result.pinned).toHaveLength(1);
76
+ expect(result.pinned[0].id).toBe(agentId);
77
+ expect(result.pinned[0].pinned).toBe(true);
78
+ });
79
+
80
+ it('should return mixed agents with and without session relationships', async () => {
81
+ // Agent with session
82
+ await clientDB.transaction(async (tx) => {
83
+ await tx.insert(Schema.agents).values({
84
+ id: 'with-session',
85
+ userId,
86
+ title: 'Agent With Session',
87
+ pinned: false,
88
+ virtual: false,
89
+ });
90
+ await tx.insert(Schema.sessions).values({
91
+ id: 'session-1',
92
+ slug: 'session-1',
93
+ userId,
94
+ });
95
+ await tx.insert(Schema.agentsToSessions).values({
96
+ agentId: 'with-session',
97
+ sessionId: 'session-1',
98
+ userId,
99
+ });
100
+ });
101
+
102
+ // Agent without session (e.g., duplicated)
103
+ await clientDB.insert(Schema.agents).values({
104
+ id: 'without-session',
105
+ userId,
106
+ title: 'Agent Without Session',
107
+ pinned: false,
108
+ virtual: false,
109
+ });
110
+
111
+ const result = await homeRepo.getSidebarAgentList();
112
+
113
+ // Both agents should appear
114
+ expect(result.ungrouped).toHaveLength(2);
115
+ expect(result.ungrouped.map((a) => a.id)).toContain('with-session');
116
+ expect(result.ungrouped.map((a) => a.id)).toContain('without-session');
117
+ });
118
+
39
119
  it('should return agents with pinned status from agents table', async () => {
40
120
  // Create an agent with pinned=true
41
121
  const agentId = 'agent-1';
@@ -380,6 +460,16 @@ describe('HomeRepository', () => {
380
460
  sessionId: 'session-search-unpinned',
381
461
  userId,
382
462
  });
463
+
464
+ // Agent without session (e.g., duplicated agent)
465
+ await tx.insert(Schema.agents).values({
466
+ id: 'search-standalone',
467
+ userId,
468
+ title: 'Standalone Searchable Agent',
469
+ description: 'A standalone agent without session',
470
+ pinned: false,
471
+ virtual: false,
472
+ });
383
473
  });
384
474
  });
385
475
 
@@ -388,6 +478,24 @@ describe('HomeRepository', () => {
388
478
  expect(result).toEqual([]);
389
479
  });
390
480
 
481
+ it('should search agents without agentsToSessions relationship', async () => {
482
+ const result = await homeRepo.searchAgents('Standalone');
483
+
484
+ expect(result).toHaveLength(1);
485
+ expect(result[0].id).toBe('search-standalone');
486
+ expect(result[0].title).toBe('Standalone Searchable Agent');
487
+ });
488
+
489
+ it('should search and return mixed agents with and without session relationships', async () => {
490
+ // Search for "Searchable" should return all 3 agents
491
+ const result = await homeRepo.searchAgents('Searchable');
492
+
493
+ expect(result).toHaveLength(3);
494
+ expect(result.map((a) => a.id)).toContain('search-pinned');
495
+ expect(result.map((a) => a.id)).toContain('search-unpinned');
496
+ expect(result.map((a) => a.id)).toContain('search-standalone');
497
+ });
498
+
391
499
  it('should search agents by title and return correct pinned status', async () => {
392
500
  const result = await homeRepo.searchAgents('Searchable Pinned');
393
501
 
@@ -407,15 +515,19 @@ describe('HomeRepository', () => {
407
515
  it('should return multiple matching agents with correct pinned status', async () => {
408
516
  const result = await homeRepo.searchAgents('Searchable');
409
517
 
410
- expect(result).toHaveLength(2);
518
+ // 3 agents: search-pinned, search-unpinned, search-standalone
519
+ expect(result).toHaveLength(3);
411
520
 
412
521
  const pinnedAgent = result.find((a) => a.id === 'search-pinned');
413
522
  const unpinnedAgent = result.find((a) => a.id === 'search-unpinned');
523
+ const standaloneAgent = result.find((a) => a.id === 'search-standalone');
414
524
 
415
525
  expect(pinnedAgent).toBeDefined();
416
526
  expect(pinnedAgent!.pinned).toBe(true);
417
527
  expect(unpinnedAgent).toBeDefined();
418
528
  expect(unpinnedAgent!.pinned).toBe(false);
529
+ expect(standaloneAgent).toBeDefined();
530
+ expect(standaloneAgent!.pinned).toBe(false);
419
531
  });
420
532
 
421
533
  it('should not return virtual agents in search', async () => {
@@ -1,6 +1,6 @@
1
1
  import { SidebarAgentItem, SidebarAgentListResponse, SidebarGroup } from '@lobechat/types';
2
2
  import { cleanObject } from '@lobechat/utils';
3
- import { and, desc, eq, ilike, inArray, or } from 'drizzle-orm';
3
+ import { and, desc, eq, ilike, inArray, not, or } from 'drizzle-orm';
4
4
 
5
5
  import {
6
6
  agents,
@@ -36,11 +36,7 @@ export class HomeRepository {
36
36
  * Get sidebar agent list with pinned, grouped, and ungrouped items
37
37
  */
38
38
  async getSidebarAgentList(): Promise<SidebarAgentListResponse> {
39
- // 1. Query all agents (non-virtual) with their session info
40
- // Note: We query both agents.pinned and sessions.pinned for backward compatibility
41
- // agents.pinned takes priority, falling back to sessions.pinned for legacy data
42
- // Note: We query both agents.sessionGroupId and sessions.groupId for backward compatibility
43
- // agents.sessionGroupId takes priority, falling back to sessions.groupId for legacy data
39
+ // 1. Query all agents (non-virtual) with their session info (if exists)
44
40
  const agentList = await this.db
45
41
  .select({
46
42
  agentSessionGroupId: agents.sessionGroupId,
@@ -55,9 +51,9 @@ export class HomeRepository {
55
51
  updatedAt: agents.updatedAt,
56
52
  })
57
53
  .from(agents)
58
- .innerJoin(agentsToSessions, eq(agents.id, agentsToSessions.agentId))
59
- .innerJoin(sessions, eq(agentsToSessions.sessionId, sessions.id))
60
- .where(and(eq(agents.userId, this.userId), eq(agents.virtual, false)))
54
+ .leftJoin(agentsToSessions, eq(agents.id, agentsToSessions.agentId))
55
+ .leftJoin(sessions, eq(agentsToSessions.sessionId, sessions.id))
56
+ .where(and(eq(agents.userId, this.userId), not(eq(agents.virtual, true))))
61
57
  .orderBy(desc(agents.updatedAt));
62
58
 
63
59
  // 2. Query all chatGroups (group chats)
@@ -75,32 +71,7 @@ export class HomeRepository {
75
71
  .orderBy(desc(chatGroups.updatedAt));
76
72
 
77
73
  // 2.1 Query member avatars for each chat group
78
- const chatGroupIds = chatGroupList.map((g) => g.id);
79
- const memberAvatarsMap = new Map<string, Array<{ avatar: string; background?: string }>>();
80
-
81
- if (chatGroupIds.length > 0) {
82
- const memberAvatars = await this.db
83
- .select({
84
- avatar: agents.avatar,
85
- backgroundColor: agents.backgroundColor,
86
- chatGroupId: chatGroupsAgents.chatGroupId,
87
- })
88
- .from(chatGroupsAgents)
89
- .innerJoin(agents, eq(chatGroupsAgents.agentId, agents.id))
90
- .where(inArray(chatGroupsAgents.chatGroupId, chatGroupIds))
91
- .orderBy(chatGroupsAgents.order);
92
-
93
- for (const member of memberAvatars) {
94
- const existing = memberAvatarsMap.get(member.chatGroupId) || [];
95
- if (member.avatar) {
96
- existing.push({
97
- avatar: member.avatar,
98
- background: member.backgroundColor ?? undefined,
99
- });
100
- }
101
- memberAvatarsMap.set(member.chatGroupId, existing);
102
- }
103
- }
74
+ const memberAvatarsMap = await this.getChatGroupMemberAvatars(chatGroupList.map((g) => g.id));
104
75
 
105
76
  // 3. Query all sessionGroups (user-defined folders)
106
77
  const groupList = await this.db
@@ -125,7 +96,7 @@ export class HomeRepository {
125
96
  id: string;
126
97
  pinned: boolean | null;
127
98
  sessionGroupId: string | null;
128
- sessionId: string;
99
+ sessionId: string | null;
129
100
  sessionPinned: boolean | null;
130
101
  title: string | null;
131
102
  updatedAt: Date;
@@ -217,7 +188,6 @@ export class HomeRepository {
217
188
  const searchPattern = `%${keyword.toLowerCase()}%`;
218
189
 
219
190
  // 1. Search agents by title or description
220
- // Note: We query both agents.pinned and sessions.pinned for backward compatibility
221
191
  const agentResults = await this.db
222
192
  .select({
223
193
  avatar: agents.avatar,
@@ -230,12 +200,12 @@ export class HomeRepository {
230
200
  updatedAt: agents.updatedAt,
231
201
  })
232
202
  .from(agents)
233
- .innerJoin(agentsToSessions, eq(agents.id, agentsToSessions.agentId))
234
- .innerJoin(sessions, eq(agentsToSessions.sessionId, sessions.id))
203
+ .leftJoin(agentsToSessions, eq(agents.id, agentsToSessions.agentId))
204
+ .leftJoin(sessions, eq(agentsToSessions.sessionId, sessions.id))
235
205
  .where(
236
206
  and(
237
207
  eq(agents.userId, this.userId),
238
- eq(agents.virtual, false),
208
+ not(eq(agents.virtual, true)),
239
209
  or(ilike(agents.title, searchPattern), ilike(agents.description, searchPattern)),
240
210
  ),
241
211
  )
@@ -260,35 +230,11 @@ export class HomeRepository {
260
230
  .orderBy(desc(chatGroups.updatedAt));
261
231
 
262
232
  // 2.1 Query member avatars for matching chat groups
263
- const chatGroupIds = chatGroupResults.map((g) => g.id);
264
- const memberAvatarsMap = new Map<string, Array<{ avatar: string; background?: string }>>();
265
-
266
- if (chatGroupIds.length > 0) {
267
- const memberAvatars = await this.db
268
- .select({
269
- avatar: agents.avatar,
270
- backgroundColor: agents.backgroundColor,
271
- chatGroupId: chatGroupsAgents.chatGroupId,
272
- })
273
- .from(chatGroupsAgents)
274
- .innerJoin(agents, eq(chatGroupsAgents.agentId, agents.id))
275
- .where(inArray(chatGroupsAgents.chatGroupId, chatGroupIds))
276
- .orderBy(chatGroupsAgents.order);
277
-
278
- for (const member of memberAvatars) {
279
- const existing = memberAvatarsMap.get(member.chatGroupId) || [];
280
- if (member.avatar) {
281
- existing.push({
282
- avatar: member.avatar,
283
- background: member.backgroundColor ?? undefined,
284
- });
285
- }
286
- memberAvatarsMap.set(member.chatGroupId, existing);
287
- }
288
- }
233
+ const memberAvatarsMap = await this.getChatGroupMemberAvatars(
234
+ chatGroupResults.map((g) => g.id),
235
+ );
289
236
 
290
237
  // 3. Combine and format results
291
- // For pinned status: agents.pinned takes priority, fallback to sessions.pinned for backward compatibility
292
238
  const results: SidebarAgentItem[] = [
293
239
  ...agentResults.map((a) =>
294
240
  cleanObject({
@@ -320,4 +266,39 @@ export class HomeRepository {
320
266
 
321
267
  return results;
322
268
  }
269
+
270
+ /**
271
+ * Query member avatars for chat groups
272
+ */
273
+ private async getChatGroupMemberAvatars(
274
+ chatGroupIds: string[],
275
+ ): Promise<Map<string, Array<{ avatar: string; background?: string }>>> {
276
+ const memberAvatarsMap = new Map<string, Array<{ avatar: string; background?: string }>>();
277
+
278
+ if (chatGroupIds.length === 0) return memberAvatarsMap;
279
+
280
+ const memberAvatars = await this.db
281
+ .select({
282
+ avatar: agents.avatar,
283
+ backgroundColor: agents.backgroundColor,
284
+ chatGroupId: chatGroupsAgents.chatGroupId,
285
+ })
286
+ .from(chatGroupsAgents)
287
+ .innerJoin(agents, eq(chatGroupsAgents.agentId, agents.id))
288
+ .where(inArray(chatGroupsAgents.chatGroupId, chatGroupIds))
289
+ .orderBy(chatGroupsAgents.order);
290
+
291
+ for (const member of memberAvatars) {
292
+ const existing = memberAvatarsMap.get(member.chatGroupId) || [];
293
+ if (member.avatar) {
294
+ existing.push({
295
+ avatar: member.avatar,
296
+ background: member.backgroundColor ?? undefined,
297
+ });
298
+ }
299
+ memberAvatarsMap.set(member.chatGroupId, existing);
300
+ }
301
+
302
+ return memberAvatarsMap;
303
+ }
323
304
  }
@@ -6,6 +6,7 @@ packages:
6
6
 
7
7
  onlyBuiltDependencies:
8
8
  - '@vercel/speed-insights'
9
+ - '@lobehub/editor'
9
10
 
10
11
  overrides:
11
12
  '@lobehub/chat-plugin-sdk>swagger-client': 3.36.0
@@ -1,8 +1,8 @@
1
1
  import { Accordion, Flexbox } from '@lobehub/ui';
2
2
  import React, { memo } from 'react';
3
3
 
4
+ import CronTopicList from './Cron';
4
5
  import Topic from './Topic';
5
- import CronTopicList from './Topic/CronTopicList';
6
6
 
7
7
  export enum ChatSidebarKey {
8
8
  CronTopics = 'cronTopics',
@@ -0,0 +1,84 @@
1
+ 'use client';
2
+
3
+ import { AccordionItem, ActionIcon, Flexbox, Icon, Text } from '@lobehub/ui';
4
+ import { Settings2Icon, TimerIcon, TimerOffIcon } from 'lucide-react';
5
+ import { memo, useCallback } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { useParams } from 'react-router-dom';
8
+
9
+ import { useRouter } from '@/app/[variants]/(main)/hooks/useRouter';
10
+ import type { AgentCronJob } from '@/database/schemas/agentCronJob';
11
+
12
+ import CronTopicItem from './CronTopicItem';
13
+
14
+ interface CronTopicGroupProps {
15
+ cronJob: AgentCronJob | null;
16
+ cronJobId: string;
17
+ topics: Array<{
18
+ createdAt: Date | string;
19
+ favorite?: boolean | null;
20
+ historySummary?: string | null;
21
+ id: string;
22
+ metadata?: any;
23
+ title?: string | null;
24
+ trigger?: string | null;
25
+ updatedAt: Date | string;
26
+ }>;
27
+ }
28
+
29
+ const CronTopicGroup = memo<CronTopicGroupProps>(({ cronJob, cronJobId, topics }) => {
30
+ const { t } = useTranslation('setting');
31
+ const { aid, cronId } = useParams<{ aid?: string; cronId?: string }>();
32
+ const router = useRouter();
33
+
34
+ const handleOpenCronJob = useCallback(() => {
35
+ if (!aid) return;
36
+ router.push(`/agent/${aid}/cron/${cronJobId}`);
37
+ }, [aid, cronJobId, router]);
38
+
39
+ const cronJobName = cronJob?.name || t('agentCronJobs.unnamedTask');
40
+ const isEnabled = cronJob?.enabled ?? false;
41
+ const isActive = cronId === cronJobId;
42
+
43
+ return (
44
+ <AccordionItem
45
+ action={
46
+ <ActionIcon
47
+ icon={Settings2Icon}
48
+ onClick={handleOpenCronJob}
49
+ size="small"
50
+ title={t('agentCronJobs.editJob')}
51
+ />
52
+ }
53
+ itemKey={cronJobId}
54
+ paddingBlock={4}
55
+ paddingInline={'8px 4px'}
56
+ title={
57
+ <Flexbox align="center" gap={6} height={24} horizontal style={{ overflow: 'hidden' }}>
58
+ <Icon icon={isEnabled ? TimerIcon : TimerOffIcon} style={{ opacity: 0.5 }} />
59
+ <Text ellipsis style={{ flex: 1 }} type={isActive ? undefined : 'secondary'}>
60
+ {cronJobName}
61
+ </Text>
62
+ {topics.length > 0 && (
63
+ <Text fontSize={11} type="secondary">
64
+ {topics.length}
65
+ </Text>
66
+ )}
67
+ </Flexbox>
68
+ }
69
+ variant={isActive ? 'filled' : 'borderless'}
70
+ >
71
+ <Flexbox gap={1} paddingBlock={1}>
72
+ {topics.length > 0 ? (
73
+ topics.map((topic) => <CronTopicItem key={topic.id} topic={topic} />)
74
+ ) : (
75
+ <Text fontSize={12} style={{ padding: '8px 12px' }} type="secondary">
76
+ {t('agentCronJobs.noExecutionResults')}
77
+ </Text>
78
+ )}
79
+ </Flexbox>
80
+ </AccordionItem>
81
+ );
82
+ });
83
+
84
+ export default CronTopicGroup;
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
5
5
 
6
6
  import { useChatStore } from '@/store/chat';
7
7
 
8
- import TopicItem from '../List/Item';
8
+ import TopicItem from '../Topic/List/Item';
9
9
 
10
10
  interface CronTopicItemProps {
11
11
  topic: {
@@ -1,9 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
4
- import { AccordionItem, ActionIcon, Flexbox, Icon, Text } from '@lobehub/ui';
5
- import { message } from 'antd';
6
- import { Calendar, Plus } from 'lucide-react';
4
+ import { Accordion, AccordionItem, ActionIcon, Flexbox, Text } from '@lobehub/ui';
5
+ import { Plus } from 'lucide-react';
7
6
  import { memo, useCallback } from 'react';
8
7
  import { useTranslation } from 'react-i18next';
9
8
  import urlJoin from 'url-join';
@@ -11,9 +10,7 @@ import urlJoin from 'url-join';
11
10
  import NeuralNetworkLoading from '@/components/NeuralNetworkLoading';
12
11
  import EmptyNavItem from '@/features/NavPanel/components/EmptyNavItem';
13
12
  import SkeletonList from '@/features/NavPanel/components/SkeletonList';
14
- import { useFetchCronTopicsWithJobInfo } from '@/hooks/useFetchCronTopicsWithJobInfo';
15
13
  import { useQueryRoute } from '@/hooks/useQueryRoute';
16
- import { agentCronJobService } from '@/services/agentCronJob';
17
14
  import { useAgentStore } from '@/store/agent';
18
15
 
19
16
  import CronTopicGroup from './CronTopicGroup';
@@ -25,33 +22,22 @@ interface CronTopicListProps {
25
22
  const CronTopicList = memo<CronTopicListProps>(({ itemKey }) => {
26
23
  const { t } = useTranslation('setting');
27
24
  const router = useQueryRoute();
28
- const agentId = useAgentStore((s) => s.activeAgentId);
29
- const { cronTopicsGroupsWithJobInfo, isLoading, mutate } = useFetchCronTopicsWithJobInfo();
30
- const totalTopics = cronTopicsGroupsWithJobInfo.reduce(
31
- (acc, group) => acc + group.topics.length,
32
- 0,
33
- );
25
+ const [agentId, createAgentCronJob, useFetchCronTopicsWithJobInfo] = useAgentStore((s) => [
26
+ s.activeAgentId,
27
+ s.createAgentCronJob,
28
+ s.useFetchCronTopicsWithJobInfo,
29
+ ]);
30
+ const { data: cronTopicsGroupsWithJobInfo = [], isLoading } =
31
+ useFetchCronTopicsWithJobInfo(agentId);
34
32
 
35
33
  const handleCreateCronJob = useCallback(async () => {
36
34
  if (!agentId) return;
37
- try {
38
- const result = await agentCronJobService.create({
39
- agentId,
40
- content: t('agentCronJobs.form.content.placeholder') || 'This is a cron job',
41
- cronPattern: '*/30 * * * *',
42
- enabled: true,
43
- name: t('agentCronJobs.addJob') || 'Cron Job Task',
44
- });
45
35
 
46
- if (result.success) {
47
- await mutate();
48
- router.push(urlJoin('/agent', agentId, 'cron', result.data.id));
49
- }
50
- } catch (error) {
51
- console.error('Failed to create cron job:', error);
52
- message.error('Failed to create scheduled task');
36
+ const cronJobId = await createAgentCronJob();
37
+ if (cronJobId) {
38
+ router.push(urlJoin('/agent', agentId, 'cron', cronJobId));
53
39
  }
54
- }, [agentId, mutate, router, t]);
40
+ }, [agentId, createAgentCronJob, router]);
55
41
 
56
42
  if (!ENABLE_BUSINESS_FEATURES) return null;
57
43
 
@@ -74,7 +60,6 @@ const CronTopicList = memo<CronTopicListProps>(({ itemKey }) => {
74
60
  paddingInline={'8px 4px'}
75
61
  title={
76
62
  <Flexbox align="center" gap={4} horizontal>
77
- <Icon icon={Calendar} size={12} />
78
63
  <Text ellipsis fontSize={12} type={'secondary'} weight={500}>
79
64
  {t('agentCronJobs.title')}
80
65
  </Text>
@@ -96,7 +81,6 @@ const CronTopicList = memo<CronTopicListProps>(({ itemKey }) => {
96
81
  paddingInline={'8px 4px'}
97
82
  title={
98
83
  <Flexbox align="center" gap={4} horizontal>
99
- <Icon icon={Calendar} size={12} />
100
84
  <Text ellipsis fontSize={12} type={'secondary'} weight={500}>
101
85
  {t('agentCronJobs.title')}
102
86
  </Text>
@@ -108,6 +92,8 @@ const CronTopicList = memo<CronTopicListProps>(({ itemKey }) => {
108
92
  );
109
93
  }
110
94
 
95
+ const totalCronJobs = cronTopicsGroupsWithJobInfo.length;
96
+
111
97
  return (
112
98
  <AccordionItem
113
99
  action={addAction}
@@ -116,14 +102,18 @@ const CronTopicList = memo<CronTopicListProps>(({ itemKey }) => {
116
102
  paddingInline={'8px 4px'}
117
103
  title={
118
104
  <Flexbox align="center" gap={4} horizontal>
119
- <Icon icon={Calendar} size={12} />
120
105
  <Text ellipsis fontSize={12} type={'secondary'} weight={500}>
121
- {`${t('agentCronJobs.title')} ${totalTopics > 0 ? totalTopics : ''}`}
106
+ {t('agentCronJobs.title')}
122
107
  </Text>
108
+ {totalCronJobs > 0 && (
109
+ <Text fontSize={11} type="secondary">
110
+ {totalCronJobs}
111
+ </Text>
112
+ )}
123
113
  </Flexbox>
124
114
  }
125
115
  >
126
- <Flexbox gap={2} paddingBlock={2}>
116
+ <Accordion defaultExpandedKeys={cronTopicsGroupsWithJobInfo.map((g) => g.cronJobId)} gap={2}>
127
117
  {cronTopicsGroupsWithJobInfo.map((group) => (
128
118
  <CronTopicGroup
129
119
  cronJob={group.cronJob}
@@ -132,7 +122,7 @@ const CronTopicList = memo<CronTopicListProps>(({ itemKey }) => {
132
122
  topics={group.topics}
133
123
  />
134
124
  ))}
135
- </Flexbox>
125
+ </Accordion>
136
126
  </AccordionItem>
137
127
  );
138
128
  });
@@ -1,7 +1,6 @@
1
- import { Input, type InputProps, Popover } from '@lobehub/ui';
2
- import type { InputRef } from 'antd';
3
- import { memo, useCallback, useEffect, useRef, useState } from 'react';
1
+ import { memo, useCallback } from 'react';
4
2
 
3
+ import InlineRename from '@/components/InlineRename';
5
4
  import { useChatStore } from '@/store/chat';
6
5
 
7
6
  interface EditingProps {
@@ -10,27 +9,14 @@ interface EditingProps {
10
9
  toggleEditing: (visible?: boolean) => void;
11
10
  }
12
11
 
13
- function FocusableInput({ ...props }: InputProps) {
14
- const ref = useRef<InputRef>(null);
15
- useEffect(() => {
16
- queueMicrotask(() => {
17
- if (ref.current) {
18
- ref.current.input?.focus();
19
- }
20
- });
21
- }, []);
22
- return <Input {...props} ref={ref} />;
23
- }
24
-
25
12
  const Editing = memo<EditingProps>(({ id, title, toggleEditing }) => {
26
- const [newTitle, setNewTitle] = useState(title);
27
13
  const [editing, updateTopicTitle] = useChatStore((s) => [
28
14
  s.topicRenamingId === id,
29
15
  s.updateTopicTitle,
30
16
  ]);
31
17
 
32
- const handleUpdate = useCallback(async () => {
33
- if (newTitle && title !== newTitle) {
18
+ const handleSave = useCallback(
19
+ async (newTitle: string) => {
34
20
  try {
35
21
  // Set loading state
36
22
  useChatStore.setState(
@@ -53,40 +39,17 @@ const Editing = memo<EditingProps>(({ id, title, toggleEditing }) => {
53
39
  'clearTopicUpdating',
54
40
  );
55
41
  }
56
- }
57
- }, [newTitle, title, id, updateTopicTitle, toggleEditing]);
42
+ },
43
+ [id, updateTopicTitle],
44
+ );
58
45
 
59
46
  return (
60
- <Popover
61
- content={
62
- <FocusableInput
63
- defaultValue={title}
64
- onBlur={handleUpdate}
65
- onChange={(e) => setNewTitle(e.target.value)}
66
- onClick={(e) => e.stopPropagation()}
67
- onPressEnter={() => {
68
- handleUpdate();
69
- toggleEditing(false);
70
- }}
71
- />
72
- }
73
- onOpenChange={(open) => {
74
- if (!open) handleUpdate();
75
-
76
- toggleEditing(open);
77
- }}
47
+ <InlineRename
48
+ onOpenChange={(open) => toggleEditing(open)}
49
+ onSave={handleSave}
78
50
  open={editing}
79
- placement="bottomLeft"
80
- styles={{
81
- content: {
82
- padding: 4,
83
- width: 320,
84
- },
85
- }}
86
- trigger="click"
87
- >
88
- <div />
89
- </Popover>
51
+ title={title}
52
+ />
90
53
  );
91
54
  });
92
55
 
@@ -18,6 +18,8 @@ import AllTopicsDrawer from '../AllTopicsDrawer';
18
18
  import ByTimeMode from '../TopicListContent/ByTimeMode';
19
19
  import FlatMode from '../TopicListContent/FlatMode';
20
20
 
21
+ const fetchParams = { excludeTriggers: ['cron'] };
22
+
21
23
  const TopicList = memo(() => {
22
24
  const { t } = useTranslation('topic');
23
25
  const router = useQueryRoute();
@@ -32,7 +34,7 @@ const TopicList = memo(() => {
32
34
 
33
35
  const [topicDisplayMode] = useUserStore((s) => [preferenceSelectors.topicDisplayMode(s)]);
34
36
 
35
- useFetchTopics({ excludeTriggers: ['cron'] });
37
+ useFetchTopics(fetchParams);
36
38
 
37
39
  // Show skeleton when current session's topic data is not yet loaded
38
40
  if (isUndefinedTopics) return <SkeletonList />;