@lobehub/lobehub 2.0.0-next.263 → 2.0.0-next.265

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 (33) 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/core/browser/Browser.ts +14 -0
  6. package/apps/desktop/src/main/core/browser/__tests__/Browser.test.ts +32 -0
  7. package/apps/desktop/src/main/utils/permissions.ts +86 -22
  8. package/changelog/v1.json +18 -0
  9. package/package.json +2 -2
  10. package/packages/database/src/models/__tests__/agent.test.ts +165 -4
  11. package/packages/database/src/models/agent.ts +46 -0
  12. package/packages/database/src/repositories/agentGroup/index.test.ts +498 -0
  13. package/packages/database/src/repositories/agentGroup/index.ts +150 -0
  14. package/packages/database/src/repositories/home/__tests__/index.test.ts +113 -1
  15. package/packages/database/src/repositories/home/index.ts +48 -67
  16. package/pnpm-workspace.yaml +1 -0
  17. package/src/app/[variants]/(main)/agent/features/Conversation/MainChatInput/index.tsx +2 -2
  18. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/index.tsx +2 -6
  19. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/useDropdownMenu.tsx +100 -0
  20. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/index.tsx +2 -4
  21. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/useDropdownMenu.tsx +149 -0
  22. package/src/app/[variants]/(main)/home/_layout/hooks/index.ts +0 -1
  23. package/src/app/[variants]/(main)/home/features/InputArea/index.tsx +1 -1
  24. package/src/features/ChatInput/InputEditor/index.tsx +1 -0
  25. package/src/features/EditorCanvas/DiffAllToolbar.tsx +1 -1
  26. package/src/server/routers/lambda/agent.ts +15 -0
  27. package/src/server/routers/lambda/agentGroup.ts +16 -0
  28. package/src/services/agent.ts +11 -0
  29. package/src/services/chatGroup/index.ts +11 -0
  30. package/src/store/home/slices/sidebarUI/action.test.ts +23 -22
  31. package/src/store/home/slices/sidebarUI/action.ts +37 -9
  32. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/Item/useDropdownMenu.tsx +0 -62
  33. package/src/app/[variants]/(main)/home/_layout/hooks/useSessionItemMenuItems.tsx +0 -238
@@ -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
@@ -11,10 +11,10 @@ import { useSendMenuItems } from './useSendMenuItems';
11
11
  const leftActions: ActionKeys[] = [
12
12
  'model',
13
13
  'search',
14
- 'typo',
15
14
  'fileUpload',
15
+ 'tools',
16
16
  '---',
17
- ['tools', 'params', 'clear'],
17
+ ['typo', 'params', 'clear'],
18
18
  'mainToken',
19
19
  ];
20
20
 
@@ -13,8 +13,8 @@ import { useGlobalStore } from '@/store/global';
13
13
  import { useHomeStore } from '@/store/home';
14
14
 
15
15
  import Actions from '../Item/Actions';
16
- import { useDropdownMenu } from '../Item/useDropdownMenu';
17
16
  import Editing from './Editing';
17
+ import { useGroupDropdownMenu } from './useDropdownMenu';
18
18
 
19
19
  interface GroupItemProps {
20
20
  className?: string;
@@ -85,13 +85,9 @@ const GroupItem = memo<GroupItemProps>(({ item, style, className }) => {
85
85
  return <GroupAvatar avatars={(avatar as any) || []} size={22} />;
86
86
  }, [isUpdating, avatar]);
87
87
 
88
- const dropdownMenu = useDropdownMenu({
89
- group: undefined,
88
+ const dropdownMenu = useGroupDropdownMenu({
90
89
  id,
91
- openCreateGroupModal: () => {}, // Groups don't need this
92
- parentType: 'group',
93
90
  pinned: pinned ?? false,
94
- sessionType: 'group',
95
91
  toggleEditing,
96
92
  });
97
93
 
@@ -0,0 +1,100 @@
1
+ import { Icon, type MenuProps } from '@lobehub/ui';
2
+ import { App } from 'antd';
3
+ import { LucideCopy, Pen, PictureInPicture2Icon, Pin, PinOff, Trash } from 'lucide-react';
4
+ import { useMemo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ import { useGlobalStore } from '@/store/global';
8
+ import { useHomeStore } from '@/store/home';
9
+
10
+ interface UseGroupDropdownMenuParams {
11
+ id: string;
12
+ pinned: boolean;
13
+ toggleEditing: (visible?: boolean) => void;
14
+ }
15
+
16
+ export const useGroupDropdownMenu = ({
17
+ id,
18
+ pinned,
19
+ toggleEditing,
20
+ }: UseGroupDropdownMenuParams): (() => MenuProps['items']) => {
21
+ const { t } = useTranslation('chat');
22
+ const { modal, message } = App.useApp();
23
+
24
+ const openAgentInNewWindow = useGlobalStore((s) => s.openAgentInNewWindow);
25
+ const [pinAgentGroup, duplicateAgentGroup, removeAgentGroup] = useHomeStore((s) => [
26
+ s.pinAgentGroup,
27
+ s.duplicateAgentGroup,
28
+ s.removeAgentGroup,
29
+ ]);
30
+
31
+ return useMemo(
32
+ () => () =>
33
+ [
34
+ {
35
+ icon: <Icon icon={pinned ? PinOff : Pin} />,
36
+ key: 'pin',
37
+ label: t(pinned ? 'pinOff' : 'pin'),
38
+ onClick: () => pinAgentGroup(id, !pinned),
39
+ },
40
+ {
41
+ icon: <Icon icon={Pen} />,
42
+ key: 'rename',
43
+ label: t('rename', { ns: 'common' }),
44
+ onClick: (info: any) => {
45
+ info.domEvent?.stopPropagation();
46
+ toggleEditing(true);
47
+ },
48
+ },
49
+ {
50
+ icon: <Icon icon={LucideCopy} />,
51
+ key: 'duplicate',
52
+ label: t('duplicate', { ns: 'common' }),
53
+ onClick: ({ domEvent }: any) => {
54
+ domEvent.stopPropagation();
55
+ duplicateAgentGroup(id);
56
+ },
57
+ },
58
+ {
59
+ icon: <Icon icon={PictureInPicture2Icon} />,
60
+ key: 'openInNewWindow',
61
+ label: t('openInNewWindow'),
62
+ onClick: ({ domEvent }: any) => {
63
+ domEvent.stopPropagation();
64
+ openAgentInNewWindow(id);
65
+ },
66
+ },
67
+ { type: 'divider' },
68
+ {
69
+ danger: true,
70
+ icon: <Icon icon={Trash} />,
71
+ key: 'delete',
72
+ label: t('delete', { ns: 'common' }),
73
+ onClick: ({ domEvent }: any) => {
74
+ domEvent.stopPropagation();
75
+ modal.confirm({
76
+ centered: true,
77
+ okButtonProps: { danger: true },
78
+ onOk: async () => {
79
+ await removeAgentGroup(id);
80
+ message.success(t('confirmRemoveGroupSuccess'));
81
+ },
82
+ title: t('confirmRemoveChatGroupItemAlert'),
83
+ });
84
+ },
85
+ },
86
+ ] as MenuProps['items'],
87
+ [
88
+ t,
89
+ pinned,
90
+ pinAgentGroup,
91
+ id,
92
+ toggleEditing,
93
+ duplicateAgentGroup,
94
+ openAgentInNewWindow,
95
+ modal,
96
+ removeAgentGroup,
97
+ message,
98
+ ],
99
+ );
100
+ };
@@ -15,9 +15,9 @@ import { useHomeStore } from '@/store/home';
15
15
 
16
16
  import { useAgentModal } from '../../ModalProvider';
17
17
  import Actions from '../Item/Actions';
18
- import { useDropdownMenu } from '../Item/useDropdownMenu';
19
18
  import Avatar from './Avatar';
20
19
  import Editing from './Editing';
20
+ import { useAgentDropdownMenu } from './useDropdownMenu';
21
21
 
22
22
  interface AgentItemProps {
23
23
  className?: string;
@@ -96,13 +96,11 @@ const AgentItem = memo<AgentItemProps>(({ item, style, className }) => {
96
96
  return <Avatar avatar={typeof avatar === 'string' ? avatar : undefined} />;
97
97
  }, [isUpdating, avatar]);
98
98
 
99
- const dropdownMenu = useDropdownMenu({
99
+ const dropdownMenu = useAgentDropdownMenu({
100
100
  group: undefined, // TODO: pass group from parent if needed
101
101
  id,
102
102
  openCreateGroupModal: handleOpenCreateGroupModal,
103
- parentType: 'agent',
104
103
  pinned: pinned ?? false,
105
- sessionType: 'agent',
106
104
  toggleEditing,
107
105
  });
108
106
 
@@ -0,0 +1,149 @@
1
+ import { SessionDefaultGroup } from '@lobechat/types';
2
+ import { Icon, type MenuProps } from '@lobehub/ui';
3
+ import { App } from 'antd';
4
+ import isEqual from 'fast-deep-equal';
5
+ import {
6
+ Check,
7
+ FolderInputIcon,
8
+ LucideCopy,
9
+ LucidePlus,
10
+ Pen,
11
+ PictureInPicture2Icon,
12
+ Pin,
13
+ PinOff,
14
+ Trash,
15
+ } from 'lucide-react';
16
+ import { useMemo } from 'react';
17
+ import { useTranslation } from 'react-i18next';
18
+
19
+ import { useGlobalStore } from '@/store/global';
20
+ import { useHomeStore } from '@/store/home';
21
+ import { homeAgentListSelectors } from '@/store/home/selectors';
22
+
23
+ interface UseAgentDropdownMenuParams {
24
+ group: string | undefined;
25
+ id: string;
26
+ openCreateGroupModal: () => void;
27
+ pinned: boolean;
28
+ toggleEditing: (visible?: boolean) => void;
29
+ }
30
+
31
+ export const useAgentDropdownMenu = ({
32
+ group,
33
+ id,
34
+ openCreateGroupModal,
35
+ pinned,
36
+ toggleEditing,
37
+ }: UseAgentDropdownMenuParams): (() => MenuProps['items']) => {
38
+ const { t } = useTranslation('chat');
39
+ const { modal, message } = App.useApp();
40
+
41
+ const openAgentInNewWindow = useGlobalStore((s) => s.openAgentInNewWindow);
42
+ const sessionCustomGroups = useHomeStore(homeAgentListSelectors.agentGroups, isEqual);
43
+ const [pinAgent, duplicateAgent, updateAgentGroup, removeAgent] = useHomeStore((s) => [
44
+ s.pinAgent,
45
+ s.duplicateAgent,
46
+ s.updateAgentGroup,
47
+ s.removeAgent,
48
+ ]);
49
+
50
+ const isDefault = group === SessionDefaultGroup.Default;
51
+
52
+ return useMemo(
53
+ () => () =>
54
+ [
55
+ {
56
+ icon: <Icon icon={pinned ? PinOff : Pin} />,
57
+ key: 'pin',
58
+ label: t(pinned ? 'pinOff' : 'pin'),
59
+ onClick: () => pinAgent(id, !pinned),
60
+ },
61
+ {
62
+ icon: <Icon icon={Pen} />,
63
+ key: 'rename',
64
+ label: t('rename', { ns: 'common' }),
65
+ onClick: (info: any) => {
66
+ info.domEvent?.stopPropagation();
67
+ toggleEditing(true);
68
+ },
69
+ },
70
+ {
71
+ icon: <Icon icon={LucideCopy} />,
72
+ key: 'duplicate',
73
+ label: t('duplicate', { ns: 'common' }),
74
+ onClick: ({ domEvent }: any) => {
75
+ domEvent.stopPropagation();
76
+ duplicateAgent(id);
77
+ },
78
+ },
79
+ {
80
+ icon: <Icon icon={PictureInPicture2Icon} />,
81
+ key: 'openInNewWindow',
82
+ label: t('openInNewWindow'),
83
+ onClick: ({ domEvent }: any) => {
84
+ domEvent.stopPropagation();
85
+ openAgentInNewWindow(id);
86
+ },
87
+ },
88
+ { type: 'divider' },
89
+ {
90
+ children: [
91
+ ...sessionCustomGroups.map(({ id: groupId, name }) => ({
92
+ icon: group === groupId ? <Icon icon={Check} /> : <div />,
93
+ key: groupId,
94
+ label: name,
95
+ onClick: () => updateAgentGroup(id, groupId),
96
+ })),
97
+ {
98
+ icon: isDefault ? <Icon icon={Check} /> : <div />,
99
+ key: 'defaultList',
100
+ label: t('defaultList'),
101
+ onClick: () => updateAgentGroup(id, SessionDefaultGroup.Default),
102
+ },
103
+ { type: 'divider' as const },
104
+ {
105
+ icon: <Icon icon={LucidePlus} />,
106
+ key: 'createGroup',
107
+ label: <div>{t('sessionGroup.createGroup')}</div>,
108
+ onClick: ({ domEvent }: any) => {
109
+ domEvent.stopPropagation();
110
+ openCreateGroupModal();
111
+ },
112
+ },
113
+ ],
114
+ icon: <Icon icon={FolderInputIcon} />,
115
+ key: 'moveGroup',
116
+ label: t('sessionGroup.moveGroup'),
117
+ },
118
+ { type: 'divider' },
119
+ {
120
+ danger: true,
121
+ icon: <Icon icon={Trash} />,
122
+ key: 'delete',
123
+ label: t('delete', { ns: 'common' }),
124
+ onClick: ({ domEvent }: any) => {
125
+ domEvent.stopPropagation();
126
+ modal.confirm({
127
+ centered: true,
128
+ okButtonProps: { danger: true },
129
+ onOk: async () => {
130
+ await removeAgent(id);
131
+ message.success(t('confirmRemoveSessionSuccess'));
132
+ },
133
+ title: t('confirmRemoveSessionItemAlert'),
134
+ });
135
+ },
136
+ },
137
+ ] as MenuProps['items'],
138
+ [
139
+ pinned,
140
+ id,
141
+ toggleEditing,
142
+ sessionCustomGroups,
143
+ group,
144
+ isDefault,
145
+ openCreateGroupModal,
146
+ message,
147
+ ],
148
+ );
149
+ };
@@ -1,4 +1,3 @@
1
1
  export { useCreateMenuItems } from './useCreateMenuItems';
2
2
  export { useProjectMenuItems } from './useProjectMenuItems';
3
3
  export { useSessionGroupMenuItems } from './useSessionGroupMenuItems';
4
- export { useSessionItemMenuItems } from './useSessionItemMenuItems';
@@ -12,7 +12,7 @@ import ModeHeader from './ModeHeader';
12
12
  import StarterList from './StarterList';
13
13
  import { useSend } from './useSend';
14
14
 
15
- const leftActions: ActionKeys[] = ['model', 'search', 'fileUpload'];
15
+ const leftActions: ActionKeys[] = ['model', 'search', 'fileUpload', 'tools'];
16
16
 
17
17
  const InputArea = () => {
18
18
  const { loading, send, inboxAgentId } = useSend();
@@ -120,6 +120,7 @@ const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
120
120
  className={className}
121
121
  content={''}
122
122
  editor={editor}
123
+ pasteAsPlainText
123
124
  {...richRenderProps}
124
125
  mentionOption={
125
126
  enableMention
@@ -36,7 +36,7 @@ const styles = createStaticStyles(({ css }) => ({
36
36
  }));
37
37
 
38
38
  const useIsEditorInit = (editor: IEditor) => {
39
- const [isEditInit, setEditInit] = useState<boolean>(!!editor.getLexicalEditor());
39
+ const [isEditInit, setEditInit] = useState<boolean>(!!editor?.getLexicalEditor());
40
40
 
41
41
  useEffect(() => {
42
42
  if (!editor) return;