@lobehub/lobehub 2.0.0-next.346 → 2.0.0-next.348

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,67 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.348](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.347...v2.0.0-next.348)
6
+
7
+ <sup>Released on **2026-01-23**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **copilot**: History popover not refreshing when agentId changes.
12
+ - **misc**: Fixed the agent group builder tools excaution edge case crash, fixed the group topic copy not right.
13
+
14
+ <br/>
15
+
16
+ <details>
17
+ <summary><kbd>Improvements and Fixes</kbd></summary>
18
+
19
+ #### What's fixed
20
+
21
+ - **copilot**: History popover not refreshing when agentId changes, closes [#11731](https://github.com/lobehub/lobe-chat/issues/11731) ([64f39e7](https://github.com/lobehub/lobe-chat/commit/64f39e7))
22
+ - **misc**: Fixed the agent group builder tools excaution edge case crash, closes [#11735](https://github.com/lobehub/lobe-chat/issues/11735) ([5de4742](https://github.com/lobehub/lobe-chat/commit/5de4742))
23
+ - **misc**: Fixed the group topic copy not right, closes [#11730](https://github.com/lobehub/lobe-chat/issues/11730) ([282c1fb](https://github.com/lobehub/lobe-chat/commit/282c1fb))
24
+
25
+ </details>
26
+
27
+ <div align="right">
28
+
29
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
30
+
31
+ </div>
32
+
33
+ ## [Version 2.0.0-next.347](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.346...v2.0.0-next.347)
34
+
35
+ <sup>Released on **2026-01-23**</sup>
36
+
37
+ #### 🐛 Bug Fixes
38
+
39
+ - **misc**: Add advace config back in agent/group profiles.
40
+
41
+ #### 💄 Styles
42
+
43
+ - **misc**: Move plugin store button outside scroll container.
44
+
45
+ <br/>
46
+
47
+ <details>
48
+ <summary><kbd>Improvements and Fixes</kbd></summary>
49
+
50
+ #### What's fixed
51
+
52
+ - **misc**: Add advace config back in agent/group profiles, closes [#11727](https://github.com/lobehub/lobe-chat/issues/11727) ([403175f](https://github.com/lobehub/lobe-chat/commit/403175f))
53
+
54
+ #### Styles
55
+
56
+ - **misc**: Move plugin store button outside scroll container, closes [#11728](https://github.com/lobehub/lobe-chat/issues/11728) ([c484d1a](https://github.com/lobehub/lobe-chat/commit/c484d1a))
57
+
58
+ </details>
59
+
60
+ <div align="right">
61
+
62
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
63
+
64
+ </div>
65
+
5
66
  ## [Version 2.0.0-next.346](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.345...v2.0.0-next.346)
6
67
 
7
68
  <sup>Released on **2026-01-23**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,25 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Fixed the agent group builder tools excaution edge case crash, fixed the group topic copy not right."
6
+ ]
7
+ },
8
+ "date": "2026-01-23",
9
+ "version": "2.0.0-next.348"
10
+ },
11
+ {
12
+ "children": {
13
+ "fixes": [
14
+ "Add advace config back in agent/group profiles."
15
+ ],
16
+ "improvements": [
17
+ "Move plugin store button outside scroll container."
18
+ ]
19
+ },
20
+ "date": "2026-01-23",
21
+ "version": "2.0.0-next.347"
22
+ },
2
23
  {
3
24
  "children": {},
4
25
  "date": "2026-01-23",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.346",
3
+ "version": "2.0.0-next.348",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -81,7 +81,7 @@ export const BatchCreateAgentsInspector = memo<
81
81
  {displayInfo && (
82
82
  <>
83
83
  <div className={styles.avatarGroup}>
84
- {displayInfo.displayAgents.map((agent, index) => (
84
+ {displayInfo.displayAgents?.map((agent, index) => (
85
85
  <Avatar
86
86
  avatar={agent.avatar}
87
87
  key={index}
@@ -2,7 +2,7 @@ import { eq, inArray } from 'drizzle-orm';
2
2
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
3
3
 
4
4
  import { getTestDB } from '../../../core/getTestDB';
5
- import { agents, messages, sessions, topics, users } from '../../../schemas';
5
+ import { agents, messagePlugins, messages, sessions, topics, users } from '../../../schemas';
6
6
  import { LobeChatDatabase } from '../../../type';
7
7
  import { CreateTopicParams, TopicModel } from '../../topic';
8
8
 
@@ -279,6 +279,129 @@ describe('TopicModel - Create', () => {
279
279
  expect(duplicatedMessages[1].content).toBe('Assistant message');
280
280
  });
281
281
 
282
+ it('should correctly map parentId references when duplicating messages', async () => {
283
+ const topicId = 'topic-with-parent-refs';
284
+
285
+ await serverDB.transaction(async (tx) => {
286
+ await tx.insert(topics).values({ id: topicId, sessionId, userId, title: 'Original Topic' });
287
+ await tx.insert(messages).values([
288
+ { id: 'msg1', role: 'user', topicId, userId, content: 'First message', parentId: null },
289
+ {
290
+ id: 'msg2',
291
+ role: 'assistant',
292
+ topicId,
293
+ userId,
294
+ content: 'Reply to first',
295
+ parentId: 'msg1',
296
+ },
297
+ {
298
+ id: 'msg3',
299
+ role: 'tool',
300
+ topicId,
301
+ userId,
302
+ content: 'Tool response',
303
+ parentId: 'msg2',
304
+ },
305
+ {
306
+ id: 'msg4',
307
+ role: 'assistant',
308
+ topicId,
309
+ userId,
310
+ content: 'Final message',
311
+ parentId: 'msg3',
312
+ },
313
+ ]);
314
+ });
315
+
316
+ const { topic: duplicatedTopic, messages: duplicatedMessages } = await topicModel.duplicate(
317
+ topicId,
318
+ 'Duplicated Topic',
319
+ );
320
+
321
+ expect(duplicatedMessages).toHaveLength(4);
322
+
323
+ const msgMap = new Map(duplicatedMessages.map((m) => [m.content, m]));
324
+ const newMsg1 = msgMap.get('First message')!;
325
+ const newMsg2 = msgMap.get('Reply to first')!;
326
+ const newMsg3 = msgMap.get('Tool response')!;
327
+ const newMsg4 = msgMap.get('Final message')!;
328
+
329
+ expect(newMsg1.parentId).toBeNull();
330
+ expect(newMsg2.parentId).toBe(newMsg1.id);
331
+ expect(newMsg3.parentId).toBe(newMsg2.id);
332
+ expect(newMsg4.parentId).toBe(newMsg3.id);
333
+
334
+ expect(newMsg1.id).not.toBe('msg1');
335
+ expect(newMsg2.id).not.toBe('msg2');
336
+ expect(newMsg3.id).not.toBe('msg3');
337
+ expect(newMsg4.id).not.toBe('msg4');
338
+ });
339
+
340
+ it('should correctly map tool_call_id when duplicating messages with tools', async () => {
341
+ const topicId = 'topic-with-tools';
342
+ const originalToolId = 'toolu_original_123';
343
+
344
+ await serverDB.transaction(async (tx) => {
345
+ await tx.insert(topics).values({ id: topicId, sessionId, userId, title: 'Original Topic' });
346
+
347
+ // Insert assistant message with tools
348
+ await tx.insert(messages).values({
349
+ id: 'msg1',
350
+ role: 'assistant',
351
+ topicId,
352
+ userId,
353
+ content: 'Using tool',
354
+ parentId: null,
355
+ tools: [{ id: originalToolId, type: 'builtin', apiName: 'broadcast' }],
356
+ });
357
+
358
+ // Insert tool message
359
+ await tx.insert(messages).values({
360
+ id: 'msg2',
361
+ role: 'tool',
362
+ topicId,
363
+ userId,
364
+ content: 'Tool response',
365
+ parentId: 'msg1',
366
+ });
367
+
368
+ // Insert messagePlugins entry
369
+ await tx.insert(messagePlugins).values({
370
+ id: 'msg2',
371
+ userId,
372
+ toolCallId: originalToolId,
373
+ apiName: 'broadcast',
374
+ });
375
+ });
376
+
377
+ const { topic: duplicatedTopic, messages: duplicatedMessages } = await topicModel.duplicate(
378
+ topicId,
379
+ 'Duplicated Topic',
380
+ );
381
+
382
+ expect(duplicatedMessages).toHaveLength(2);
383
+
384
+ const msgMap = new Map(duplicatedMessages.map((m) => [m.role, m]));
385
+ const newAssistant = msgMap.get('assistant')!;
386
+ const newTool = msgMap.get('tool')!;
387
+
388
+ // Check that tools array has new IDs
389
+ expect(newAssistant.tools).toBeDefined();
390
+ const newTools = newAssistant.tools as any[];
391
+ expect(newTools).toHaveLength(1);
392
+ expect(newTools[0].id).not.toBe(originalToolId);
393
+ expect(newTools[0].id).toMatch(/^toolu_/);
394
+
395
+ // Check that messagePlugins was copied with new toolCallId
396
+ const newPlugin = await serverDB.query.messagePlugins.findFirst({
397
+ where: eq(messagePlugins.id, newTool.id),
398
+ });
399
+
400
+ expect(newPlugin).toBeDefined();
401
+ expect(newPlugin!.toolCallId).toBe(newTools[0].id);
402
+ expect(newPlugin!.toolCallId).not.toBe(originalToolId);
403
+ });
404
+
282
405
  it('should throw an error if the topic to duplicate does not exist', async () => {
283
406
  const topicId = 'nonexistent-topic';
284
407
 
@@ -17,7 +17,14 @@ import {
17
17
  sql,
18
18
  } from 'drizzle-orm';
19
19
 
20
- import { TopicItem, agents, agentsToSessions, messages, topics } from '../schemas';
20
+ import {
21
+ TopicItem,
22
+ agents,
23
+ agentsToSessions,
24
+ messagePlugins,
25
+ messages,
26
+ topics,
27
+ } from '../schemas';
21
28
  import { LobeChatDatabase } from '../type';
22
29
  import { genEndDateWhere, genRangeWhere, genStartDateWhere, genWhere } from '../utils/genWhere';
23
30
  import { idGenerator } from '../utils/idGenerator';
@@ -498,28 +505,83 @@ export class TopicModel {
498
505
  })
499
506
  .returning();
500
507
 
501
- // Find messages associated with the original topic
508
+ // Find messages associated with the original topic, ordered by createdAt
502
509
  const originalMessages = await tx
503
510
  .select()
504
511
  .from(messages)
505
- .where(and(eq(messages.topicId, topicId), eq(messages.userId, this.userId)));
506
-
507
- // copy messages
508
- const duplicatedMessages = await Promise.all(
509
- originalMessages.map(async (message) => {
510
- const result = (await tx
511
- .insert(messages)
512
- .values({
513
- ...message,
514
- clientId: null,
515
- id: idGenerator('messages'),
516
- topicId: duplicatedTopic.id,
517
- })
518
- .returning()) as DBMessageItem[];
519
-
520
- return result[0];
521
- }),
522
- );
512
+ .where(and(eq(messages.topicId, topicId), eq(messages.userId, this.userId)))
513
+ .orderBy(messages.createdAt);
514
+
515
+ // Find all messagePlugins for this topic
516
+ const messageIds = originalMessages.map((m) => m.id);
517
+ const originalPlugins =
518
+ messageIds.length > 0
519
+ ? await tx
520
+ .select()
521
+ .from(messagePlugins)
522
+ .where(inArray(messagePlugins.id, messageIds))
523
+ : [];
524
+
525
+ // Build oldId -> newId mapping for messages
526
+ const idMap = new Map<string, string>();
527
+ originalMessages.forEach((message) => {
528
+ idMap.set(message.id, idGenerator('messages'));
529
+ });
530
+
531
+ // Build oldToolId -> newToolId mapping for tools
532
+ const toolIdMap = new Map<string, string>();
533
+ originalMessages.forEach((message) => {
534
+ if (message.tools && Array.isArray(message.tools)) {
535
+ (message.tools as any[]).forEach((tool: any) => {
536
+ if (tool.id) {
537
+ toolIdMap.set(tool.id, `toolu_${idGenerator('messages')}`);
538
+ }
539
+ });
540
+ }
541
+ });
542
+
543
+ // copy messages sequentially to respect foreign key constraints
544
+ const duplicatedMessages: DBMessageItem[] = [];
545
+ for (const message of originalMessages) {
546
+ const newId = idMap.get(message.id)!;
547
+ const newParentId = message.parentId ? idMap.get(message.parentId) || null : null;
548
+
549
+ // Update tool IDs in tools array
550
+ let newTools = message.tools;
551
+ if (newTools && Array.isArray(newTools)) {
552
+ newTools = (newTools as any[]).map((tool: any) => ({
553
+ ...tool,
554
+ id: tool.id ? toolIdMap.get(tool.id) || tool.id : tool.id,
555
+ }));
556
+ }
557
+
558
+ const result = (await tx
559
+ .insert(messages)
560
+ .values({
561
+ ...message,
562
+ clientId: null,
563
+ id: newId,
564
+ parentId: newParentId,
565
+ topicId: duplicatedTopic.id,
566
+ tools: newTools,
567
+ })
568
+ .returning()) as DBMessageItem[];
569
+
570
+ duplicatedMessages.push(result[0]);
571
+
572
+ // Copy messagePlugins if exists for this message
573
+ const plugin = originalPlugins.find((p) => p.id === message.id);
574
+ if (plugin) {
575
+ const newToolCallId = plugin.toolCallId ? toolIdMap.get(plugin.toolCallId) || null : null;
576
+
577
+ await tx.insert(messagePlugins).values({
578
+ ...plugin,
579
+ id: newId,
580
+ clientId: null,
581
+ toolCallId: newToolCallId,
582
+ });
583
+ }
584
+ }
523
585
 
524
586
  return {
525
587
  messages: duplicatedMessages,
@@ -0,0 +1,133 @@
1
+ 'use client';
2
+
3
+ import { Avatar, Block, Flexbox, Icon, Text } from '@lobehub/ui';
4
+ import { useTheme } from 'antd-style';
5
+ import type { ItemType } from 'antd/es/menu/interface';
6
+ import isEqual from 'fast-deep-equal';
7
+ import { BrainIcon, MessageSquareHeartIcon, Settings2Icon } from 'lucide-react';
8
+ import { memo, useMemo, useState } from 'react';
9
+ import { useTranslation } from 'react-i18next';
10
+
11
+ import Menu from '@/components/Menu';
12
+ import { DEFAULT_AVATAR, DEFAULT_INBOX_AVATAR } from '@/const/meta';
13
+ import { AgentSettings as Settings } from '@/features/AgentSetting';
14
+ import { useAgentStore } from '@/store/agent';
15
+ import { agentSelectors, builtinAgentSelectors } from '@/store/agent/selectors';
16
+ import { ChatSettingsTabs } from '@/store/global/initialState';
17
+
18
+ const Content = memo(() => {
19
+ const { t } = useTranslation('setting');
20
+ const theme = useTheme();
21
+ const [agentId, isInbox] = useAgentStore((s) => [
22
+ s.activeAgentId,
23
+ builtinAgentSelectors.isInboxAgent(s),
24
+ ]);
25
+ const config = useAgentStore(agentSelectors.currentAgentConfig, isEqual);
26
+ const meta = useAgentStore(agentSelectors.currentAgentMeta, isEqual);
27
+ const [tab, setTab] = useState(ChatSettingsTabs.Chat);
28
+
29
+ const updateAgentConfig = async (config: any) => {
30
+ if (!agentId) return;
31
+ await useAgentStore.getState().optimisticUpdateAgentConfig(agentId, config);
32
+ };
33
+
34
+ const updateAgentMeta = async (meta: any) => {
35
+ if (!agentId) return;
36
+ await useAgentStore.getState().optimisticUpdateAgentMeta(agentId, meta);
37
+ };
38
+
39
+ const menuItems: ItemType[] = useMemo(
40
+ () =>
41
+ [
42
+ {
43
+ icon: <Icon icon={Settings2Icon} />,
44
+ key: ChatSettingsTabs.Chat,
45
+ label: t('agentTab.chat'),
46
+ },
47
+ !isInbox
48
+ ? {
49
+ icon: <Icon icon={MessageSquareHeartIcon} />,
50
+ key: ChatSettingsTabs.Opening,
51
+ label: t('agentTab.opening'),
52
+ }
53
+ : null,
54
+ {
55
+ icon: <Icon icon={BrainIcon} />,
56
+ key: ChatSettingsTabs.Modal,
57
+ label: t('agentTab.modal'),
58
+ },
59
+ ].filter(Boolean) as ItemType[],
60
+ [t, isInbox],
61
+ );
62
+
63
+ const displayTitle = isInbox ? 'Lobe AI' : meta.title || t('defaultSession', { ns: 'common' });
64
+
65
+ return (
66
+ <Flexbox
67
+ direction="horizontal"
68
+ height="100%"
69
+ style={{
70
+ padding: 0,
71
+ position: 'relative',
72
+ }}
73
+ >
74
+ <Flexbox
75
+ height={'100%'}
76
+ paddingBlock={24}
77
+ paddingInline={8}
78
+ style={{
79
+ background: theme.colorBgLayout,
80
+ borderRight: `1px solid ${theme.colorBorderSecondary}`,
81
+ }}
82
+ width={200}
83
+ >
84
+ <Block
85
+ align={'center'}
86
+ gap={8}
87
+ horizontal
88
+ paddingBlock={'14px 16px'}
89
+ paddingInline={4}
90
+ style={{
91
+ overflow: 'hidden',
92
+ }}
93
+ variant={'borderless'}
94
+ >
95
+ <Avatar
96
+ avatar={isInbox ? DEFAULT_INBOX_AVATAR : meta.avatar || DEFAULT_AVATAR}
97
+ background={meta.backgroundColor || undefined}
98
+ shape={'square'}
99
+ size={28}
100
+ />
101
+ <Text ellipsis weight={500}>
102
+ {displayTitle}
103
+ </Text>
104
+ </Block>
105
+ <Menu
106
+ items={menuItems}
107
+ onClick={({ key }) => setTab(key as ChatSettingsTabs)}
108
+ selectable
109
+ selectedKeys={[tab]}
110
+ style={{ width: '100%' }}
111
+ />
112
+ </Flexbox>
113
+ <Flexbox
114
+ flex={1}
115
+ paddingBlock={24}
116
+ paddingInline={64}
117
+ style={{ overflow: 'scroll', width: '100%' }}
118
+ >
119
+ <Settings
120
+ config={config}
121
+ id={agentId}
122
+ loading={false}
123
+ meta={meta}
124
+ onConfigChange={updateAgentConfig}
125
+ onMetaChange={updateAgentMeta}
126
+ tab={tab}
127
+ />
128
+ </Flexbox>
129
+ </Flexbox>
130
+ );
131
+ });
132
+
133
+ export default Content;
@@ -0,0 +1,35 @@
1
+ 'use client';
2
+
3
+ import { Modal } from '@lobehub/ui';
4
+ import { memo } from 'react';
5
+
6
+ import { useAgentStore } from '@/store/agent';
7
+
8
+ import Content from './Content';
9
+
10
+ const AgentSettings = memo(() => {
11
+ const showAgentSetting = useAgentStore((s) => s.showAgentSetting);
12
+
13
+ return (
14
+ <Modal
15
+ centered
16
+ footer={null}
17
+ onCancel={() => useAgentStore.setState({ showAgentSetting: false })}
18
+ open={showAgentSetting}
19
+ styles={{
20
+ body: {
21
+ height: '60vh',
22
+ overflow: 'scroll',
23
+ padding: 0,
24
+ position: 'relative',
25
+ },
26
+ }}
27
+ title={null}
28
+ width={960}
29
+ >
30
+ <Content />
31
+ </Modal>
32
+ );
33
+ });
34
+
35
+ export default AgentSettings;
@@ -3,8 +3,9 @@
3
3
  import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
4
4
  import { Button, Flexbox } from '@lobehub/ui';
5
5
  import { Divider } from 'antd';
6
+ import { useTheme } from 'antd-style';
6
7
  import isEqual from 'fast-deep-equal';
7
- import { Clock, PlayIcon } from 'lucide-react';
8
+ import { Clock, PlayIcon, Settings2Icon } from 'lucide-react';
8
9
  import React, { memo, useCallback } from 'react';
9
10
  import { useTranslation } from 'react-i18next';
10
11
  import urlJoin from 'url-join';
@@ -16,6 +17,7 @@ import { agentSelectors } from '@/store/agent/selectors';
16
17
  import { useChatStore } from '@/store/chat';
17
18
 
18
19
  import AgentCronJobs from '../AgentCronJobs';
20
+ import AgentSettings from '../AgentSettings';
19
21
  import EditorCanvas from '../EditorCanvas';
20
22
  import AgentPublishButton from '../Header/AgentPublishButton';
21
23
  import AgentHeader from './AgentHeader';
@@ -23,6 +25,7 @@ import AgentTool from './AgentTool';
23
25
 
24
26
  const ProfileEditor = memo(() => {
25
27
  const { t } = useTranslation('setting');
28
+ const theme = useTheme();
26
29
  const config = useAgentStore(agentSelectors.currentAgentConfig, isEqual);
27
30
  const updateConfig = useAgentStore((s) => s.updateAgentConfig);
28
31
  const agentId = useAgentStore((s) => s.activeAgentId);
@@ -44,7 +47,7 @@ const ProfileEditor = memo(() => {
44
47
  >
45
48
  {/* Header: Avatar + Name + Description */}
46
49
  <AgentHeader />
47
- {/* Config Bar: Model Selector */}
50
+ {/* Config Bar: Model Selector + Settings Button */}
48
51
  <Flexbox
49
52
  align={'center'}
50
53
  gap={8}
@@ -59,6 +62,15 @@ const ProfileEditor = memo(() => {
59
62
  provider: config.provider,
60
63
  }}
61
64
  />
65
+ <Button
66
+ icon={Settings2Icon}
67
+ onClick={() => useAgentStore.setState({ showAgentSetting: true })}
68
+ size={'small'}
69
+ style={{ color: theme.colorTextSecondary }}
70
+ type={'text'}
71
+ >
72
+ {t('advancedSettings')}
73
+ </Button>
62
74
  </Flexbox>
63
75
  <AgentTool />
64
76
  <Flexbox
@@ -93,6 +105,8 @@ const ProfileEditor = memo(() => {
93
105
  <EditorCanvas />
94
106
  {/* Agent Cron Jobs Display (only show if jobs exist) */}
95
107
  {ENABLE_BUSINESS_FEATURES && <AgentCronJobs />}
108
+ {/* Advanced Settings Modal */}
109
+ <AgentSettings />
96
110
  </>
97
111
  );
98
112
  });
@@ -0,0 +1,140 @@
1
+ 'use client';
2
+
3
+ import { Avatar, Block, Flexbox, Icon, Text } from '@lobehub/ui';
4
+ import { useTheme } from 'antd-style';
5
+ import type { ItemType } from 'antd/es/menu/interface';
6
+ import { MessageSquareHeartIcon } from 'lucide-react';
7
+ import { memo, useMemo, useState } from 'react';
8
+ import { useTranslation } from 'react-i18next';
9
+
10
+ import Menu from '@/components/Menu';
11
+ import { DEFAULT_AVATAR } from '@/const/meta';
12
+ import { AgentSettings as Settings } from '@/features/AgentSetting';
13
+ import { useAgentGroupStore } from '@/store/agentGroup';
14
+ import { agentGroupSelectors } from '@/store/agentGroup/selectors';
15
+ import { ChatSettingsTabs } from '@/store/global/initialState';
16
+
17
+ const Content = memo(() => {
18
+ const { t } = useTranslation('setting');
19
+ const theme = useTheme();
20
+ const groupId = useAgentGroupStore(agentGroupSelectors.activeGroupId);
21
+ const currentGroup = useAgentGroupStore(agentGroupSelectors.currentGroup);
22
+ const [tab] = useState(ChatSettingsTabs.Opening);
23
+
24
+ const updateGroupConfig = async (config: any) => {
25
+ if (!groupId) return;
26
+ // Only update openingMessage and openingQuestions
27
+ const groupConfig = {
28
+ openingMessage: config.openingMessage,
29
+ openingQuestions: config.openingQuestions,
30
+ };
31
+ await useAgentGroupStore.getState().updateGroupConfig(groupConfig);
32
+ };
33
+
34
+ const updateGroupMeta = async (meta: any) => {
35
+ if (!groupId) return;
36
+ await useAgentGroupStore.getState().updateGroup(groupId, meta);
37
+ };
38
+
39
+ // Convert group config to agent config format for AgentSettings component
40
+ const agentConfig = useMemo(
41
+ () =>
42
+ ({
43
+ chatConfig: {},
44
+ model: '',
45
+ openingMessage: currentGroup?.config?.openingMessage,
46
+ openingQuestions: currentGroup?.config?.openingQuestions,
47
+ params: {},
48
+ systemRole: '',
49
+ tts: {},
50
+ }) as any,
51
+ [currentGroup?.config],
52
+ );
53
+
54
+ const agentMeta = useMemo(
55
+ () => ({
56
+ avatar: currentGroup?.avatar || undefined,
57
+ backgroundColor: currentGroup?.backgroundColor || undefined,
58
+ description: currentGroup?.description || undefined,
59
+ tags: [] as string[],
60
+ title: currentGroup?.title || undefined,
61
+ }),
62
+ [currentGroup],
63
+ );
64
+
65
+ const menuItems: ItemType[] = useMemo(
66
+ () => [
67
+ {
68
+ icon: <Icon icon={MessageSquareHeartIcon} />,
69
+ key: ChatSettingsTabs.Opening,
70
+ label: t('agentTab.opening'),
71
+ },
72
+ ],
73
+ [t],
74
+ );
75
+
76
+ const displayTitle = currentGroup?.title || t('defaultSession', { ns: 'common' });
77
+
78
+ return (
79
+ <Flexbox
80
+ direction="horizontal"
81
+ height="100%"
82
+ style={{
83
+ padding: 0,
84
+ position: 'relative',
85
+ }}
86
+ >
87
+ <Flexbox
88
+ height={'100%'}
89
+ paddingBlock={24}
90
+ paddingInline={8}
91
+ style={{
92
+ background: theme.colorBgLayout,
93
+ borderRight: `1px solid ${theme.colorBorderSecondary}`,
94
+ }}
95
+ width={200}
96
+ >
97
+ <Block
98
+ align={'center'}
99
+ gap={8}
100
+ horizontal
101
+ paddingBlock={'14px 16px'}
102
+ paddingInline={4}
103
+ style={{
104
+ overflow: 'hidden',
105
+ }}
106
+ variant={'borderless'}
107
+ >
108
+ <Avatar
109
+ avatar={currentGroup?.avatar || DEFAULT_AVATAR}
110
+ background={currentGroup?.backgroundColor || undefined}
111
+ shape={'square'}
112
+ size={28}
113
+ />
114
+ <Text ellipsis weight={500}>
115
+ {displayTitle}
116
+ </Text>
117
+ </Block>
118
+ <Menu items={menuItems} selectable selectedKeys={[tab]} style={{ width: '100%' }} />
119
+ </Flexbox>
120
+ <Flexbox
121
+ flex={1}
122
+ paddingBlock={24}
123
+ paddingInline={64}
124
+ style={{ overflow: 'scroll', width: '100%' }}
125
+ >
126
+ <Settings
127
+ config={agentConfig}
128
+ id={groupId}
129
+ loading={false}
130
+ meta={agentMeta}
131
+ onConfigChange={updateGroupConfig}
132
+ onMetaChange={updateGroupMeta}
133
+ tab={tab}
134
+ />
135
+ </Flexbox>
136
+ </Flexbox>
137
+ );
138
+ });
139
+
140
+ export default Content;
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+
3
+ import { Modal } from '@lobehub/ui';
4
+ import { memo } from 'react';
5
+
6
+ import Content from './Content';
7
+
8
+ interface AgentSettingsProps {
9
+ onCancel: () => void;
10
+ open: boolean;
11
+ }
12
+
13
+ const AgentSettings = memo<AgentSettingsProps>(({ open, onCancel }) => {
14
+ return (
15
+ <Modal
16
+ centered
17
+ footer={null}
18
+ onCancel={onCancel}
19
+ open={open}
20
+ styles={{
21
+ body: {
22
+ height: '60vh',
23
+ overflow: 'scroll',
24
+ padding: 0,
25
+ position: 'relative',
26
+ },
27
+ }}
28
+ title={null}
29
+ width={960}
30
+ >
31
+ <Content />
32
+ </Modal>
33
+ );
34
+ });
35
+
36
+ export default AgentSettings;
@@ -2,8 +2,9 @@
2
2
 
3
3
  import { Button, Flexbox } from '@lobehub/ui';
4
4
  import { Divider } from 'antd';
5
- import { PlayIcon } from 'lucide-react';
6
- import { memo, useCallback, useMemo } from 'react';
5
+ import { useTheme } from 'antd-style';
6
+ import { PlayIcon, Settings2Icon } from 'lucide-react';
7
+ import { memo, useCallback, useMemo, useState } from 'react';
7
8
  import { useTranslation } from 'react-i18next';
8
9
  import urlJoin from 'url-join';
9
10
 
@@ -13,12 +14,15 @@ import { useAgentGroupStore } from '@/store/agentGroup';
13
14
  import { agentGroupSelectors } from '@/store/agentGroup/selectors';
14
15
  import { useGroupProfileStore } from '@/store/groupProfile';
15
16
 
17
+ import AgentSettings from '../AgentSettings';
16
18
  import AutoSaveHint from '../Header/AutoSaveHint';
17
19
  import GroupPublishButton from '../Header/GroupPublishButton';
18
20
  import GroupHeader from './GroupHeader';
19
21
 
20
22
  const GroupProfile = memo(() => {
21
23
  const { t } = useTranslation(['setting', 'chat']);
24
+ const theme = useTheme();
25
+ const [showAgentSetting, setShowAgentSetting] = useState(false);
22
26
  const groupId = useAgentGroupStore(agentGroupSelectors.activeGroupId);
23
27
  const currentGroup = useAgentGroupStore(agentGroupSelectors.currentGroup);
24
28
  const updateGroup = useAgentGroupStore((s) => s.updateGroup);
@@ -86,6 +90,15 @@ const GroupProfile = memo(() => {
86
90
  {t('startConversation')}
87
91
  </Button>
88
92
  <GroupPublishButton />
93
+ <Button
94
+ icon={Settings2Icon}
95
+ onClick={() => setShowAgentSetting(true)}
96
+ size={'small'}
97
+ style={{ color: theme.colorTextSecondary }}
98
+ type={'text'}
99
+ >
100
+ {t('advancedSettings')}
101
+ </Button>
89
102
  </Flexbox>
90
103
  </Flexbox>
91
104
  <Divider />
@@ -97,6 +110,8 @@ const GroupProfile = memo(() => {
97
110
  onContentChange={onContentChange}
98
111
  placeholder={t('group.profile.contentPlaceholder', { ns: 'chat' })}
99
112
  />
113
+ {/* Advanced Settings Modal */}
114
+ <AgentSettings onCancel={() => setShowAgentSetting(false)} open={showAgentSetting} />
100
115
  </>
101
116
  );
102
117
  });
@@ -86,13 +86,14 @@ const OpeningQuestions = memo(() => {
86
86
  const isRepeat = openingQuestions.includes(questionInput.trim());
87
87
 
88
88
  return (
89
- <Flexbox gap={8}>
90
- <Flexbox gap={4}>
91
- <Space.Compact>
89
+ <Flexbox gap={8} width={'100%'}>
90
+ <Flexbox gap={4} width={'100%'}>
91
+ <Space.Compact style={{ width: '100%' }}>
92
92
  <Input
93
93
  onChange={(e) => setQuestionInput(e.target.value)}
94
94
  onPressEnter={addQuestion}
95
95
  placeholder={t('settingOpening.openingQuestions.placeholder')}
96
+ style={{ flex: 1 }}
96
97
  value={questionInput}
97
98
  />
98
99
  <Button
@@ -4,8 +4,6 @@ import { Form } from '@lobehub/ui';
4
4
  import { memo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
- import { FORM_STYLE } from '@/const/layoutTokens';
8
-
9
7
  import OpeningMessage from './OpeningMessage';
10
8
  import OpeningQuestions from './OpeningQuestions';
11
9
 
@@ -44,7 +42,6 @@ const AgentOpening = memo(() => {
44
42
  ]}
45
43
  itemsType={'group'}
46
44
  variant={'borderless'}
47
- {...FORM_STYLE}
48
45
  />
49
46
  );
50
47
  });
@@ -62,7 +62,7 @@ const CouncilList = memo<CouncilListProps>(({ members, displayMode, activeTab })
62
62
  minWidth: MIN_WIDTH * members.length + 32 + 32 * (members.length - 1),
63
63
  }}
64
64
  >
65
- {members.map((member, idx) => {
65
+ {members?.map((member, idx) => {
66
66
  if (!member) return null;
67
67
  return (
68
68
  <Fragment key={member.id}>
@@ -77,7 +77,7 @@ const CouncilList = memo<CouncilListProps>(({ members, displayMode, activeTab })
77
77
  >
78
78
  <CouncilMember index={idx} item={member} />
79
79
  </Flexbox>
80
- {idx < members.length - 1 && (
80
+ {idx < members?.length - 1 && (
81
81
  <Divider
82
82
  dashed
83
83
  orientation={'vertical'}
@@ -1,7 +1,7 @@
1
1
  import { ActionIcon, Block, Flexbox, Popover } from '@lobehub/ui';
2
2
  import { createStaticStyles, cx } from 'antd-style';
3
3
  import { ChevronsUpDownIcon, Clock3Icon, PanelRightCloseIcon, PlusIcon } from 'lucide-react';
4
- import { Suspense, memo, useMemo, useState } from 'react';
4
+ import { Suspense, memo, useCallback, useMemo, useState } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import AgentAvatar from '@/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/Avatar';
@@ -164,6 +164,15 @@ const CopilotToolbar = memo<CopilotToolbarProps>(({ agentId, isHovered }) => {
164
164
  const setActiveAgentId = useAgentStore((s) => s.setActiveAgentId);
165
165
  const [topicPopoverOpen, setTopicPopoverOpen] = useState(false);
166
166
 
167
+ const handleAgentChange = useCallback(
168
+ (id: string) => {
169
+ setActiveAgentId(id);
170
+ // Sync chatStore's activeAgentId to ensure topic selectors work correctly
171
+ useChatStore.setState({ activeAgentId: id });
172
+ },
173
+ [setActiveAgentId],
174
+ );
175
+
167
176
  // Fetch topics for the agent builder
168
177
  useChatStore((s) => s.useFetchTopics)(true, { agentId });
169
178
 
@@ -175,13 +184,15 @@ const CopilotToolbar = memo<CopilotToolbarProps>(({ agentId, isHovered }) => {
175
184
 
176
185
  const [toggleRightPanel] = useGlobalStore((s) => [s.toggleRightPanel]);
177
186
 
178
- const hideHistory = !topics || topics.length === 0;
187
+ // topics === undefined means still loading, topics.length === 0 means confirmed empty
188
+ const isLoadingTopics = topics === undefined;
189
+ const hideHistory = !isLoadingTopics && topics.length === 0;
179
190
 
180
191
  return (
181
192
  <NavHeader
182
193
  left={
183
194
  <Flexbox align="center" gap={8} horizontal>
184
- <AgentSelector agentId={agentId} onAgentChange={setActiveAgentId} />
195
+ <AgentSelector agentId={agentId} onAgentChange={handleAgentChange} />
185
196
  </Flexbox>
186
197
  }
187
198
  right={
@@ -218,7 +229,7 @@ const CopilotToolbar = memo<CopilotToolbarProps>(({ agentId, isHovered }) => {
218
229
  </Flexbox>
219
230
  }
220
231
  onOpenChange={setTopicPopoverOpen}
221
- open={topicPopoverOpen}
232
+ open={isLoadingTopics ? false : topicPopoverOpen}
222
233
  placement="bottomRight"
223
234
  styles={{
224
235
  content: {
@@ -228,7 +239,12 @@ const CopilotToolbar = memo<CopilotToolbarProps>(({ agentId, isHovered }) => {
228
239
  }}
229
240
  trigger="click"
230
241
  >
231
- <ActionIcon icon={Clock3Icon} size={DESKTOP_HEADER_ICON_SIZE} />
242
+ <ActionIcon
243
+ disabled={isLoadingTopics}
244
+ icon={Clock3Icon}
245
+ loading={isLoadingTopics}
246
+ size={DESKTOP_HEADER_ICON_SIZE}
247
+ />
232
248
  </Popover>
233
249
  )}
234
250
  </div>
@@ -44,13 +44,7 @@ const prefixCls = 'ant';
44
44
  const styles = createStaticStyles(({ css }) => ({
45
45
  dropdown: css`
46
46
  overflow: hidden;
47
-
48
47
  width: 100%;
49
- border: 1px solid ${cssVar.colorBorderSecondary};
50
- border-radius: ${cssVar.borderRadiusLG};
51
-
52
- background: ${cssVar.colorBgElevated};
53
- box-shadow: ${cssVar.boxShadowSecondary};
54
48
 
55
49
  .${prefixCls}-dropdown-menu {
56
50
  border-radius: 0 !important;
@@ -339,7 +333,9 @@ const AgentTool = memo<AgentToolProps>(
339
333
  () => [
340
334
  // 原有的 builtin 工具
341
335
  ...filteredBuiltinList.map((item) => ({
342
- icon: <Avatar avatar={item.meta.avatar} size={20} style={{ flex: 'none' }} />,
336
+ icon: (
337
+ <Avatar avatar={item.meta.avatar} size={20} style={{ flex: 'none', marginRight: 0 }} />
338
+ ),
343
339
  key: item.identifier,
344
340
  label: (
345
341
  <ToolItem
@@ -412,18 +408,6 @@ const AgentTool = memo<AgentToolProps>(
412
408
  ),
413
409
  type: 'group',
414
410
  },
415
- {
416
- type: 'divider',
417
- },
418
- {
419
- extra: <Icon icon={ArrowRight} />,
420
- icon: Store,
421
- key: 'plugin-store',
422
- label: t('tools.plugins.store'),
423
- onClick: () => {
424
- createSkillStoreModal();
425
- },
426
- },
427
411
  ],
428
412
  [builtinItems, pluginItems, enablePluginCount, t],
429
413
  );
@@ -557,7 +541,6 @@ const AgentTool = memo<AgentToolProps>(
557
541
  {/* Plugin Selector Dropdown - Using Action component pattern */}
558
542
  <Suspense fallback={button}>
559
543
  <ActionDropdown
560
- maxHeight={500}
561
544
  maxWidth={400}
562
545
  menu={{
563
546
  items: currentItems,
@@ -567,11 +550,10 @@ const AgentTool = memo<AgentToolProps>(
567
550
  overflowY: 'visible',
568
551
  },
569
552
  }}
570
- minHeight={isKlavisEnabledInEnv || isLobehubSkillEnabled ? 500 : undefined}
571
553
  minWidth={400}
572
554
  placement={'bottomLeft'}
573
555
  popupRender={(menu) => (
574
- <div className={styles.dropdown}>
556
+ <Flexbox className={styles.dropdown} style={{ maxHeight: 500 }}>
575
557
  {/* stopPropagation prevents dropdown's onClick from calling preventDefault on Segmented */}
576
558
  <div className={styles.header} onClick={(e) => e.stopPropagation()}>
577
559
  <Segmented
@@ -591,16 +573,26 @@ const AgentTool = memo<AgentToolProps>(
591
573
  value={effectiveTab}
592
574
  />
593
575
  </div>
594
- <div
595
- className={styles.scroller}
576
+ <div className={styles.scroller} style={{ flex: 1 }}>
577
+ {menu}
578
+ </div>
579
+ <Flexbox
580
+ align="center"
581
+ gap={8}
582
+ horizontal
583
+ onClick={() => createSkillStoreModal()}
596
584
  style={{
597
- maxHeight: 500,
598
- minHeight: isKlavisEnabledInEnv || isLobehubSkillEnabled ? 500 : undefined,
585
+ borderBlockStart: `1px solid ${cssVar.colorBorderSecondary}`,
586
+ cursor: 'pointer',
587
+ flex: 'none',
588
+ padding: cssVar.paddingSM,
599
589
  }}
600
590
  >
601
- {menu}
602
- </div>
603
- </div>
591
+ <Icon icon={Store} />
592
+ <span style={{ flex: 1 }}>{t('tools.plugins.store')}</span>
593
+ <Icon icon={ArrowRight} />
594
+ </Flexbox>
595
+ </Flexbox>
604
596
  )}
605
597
  trigger={'click'}
606
598
  >