@lobehub/lobehub 2.0.0-next.278 → 2.0.0-next.279
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 +26 -0
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/src/app/[variants]/(main)/agent/features/Conversation/ThreadHydration.tsx +2 -1
- package/src/app/[variants]/(main)/agent/features/Portal/_layout/Mobile.tsx +3 -3
- package/src/app/[variants]/(main)/group/_layout/Sidebar/GroupConfig/GroupMember.tsx +3 -2
- package/src/app/[variants]/(main)/group/features/Conversation/ThreadHydration.tsx +2 -1
- package/src/app/[variants]/(main)/group/features/Portal/_layout/Mobile.tsx +3 -3
- package/src/app/[variants]/(main)/group/profile/features/MemberProfile/AgentTool.tsx +4 -1
- package/src/features/Conversation/ChatList/index.tsx +5 -1
- package/src/features/Conversation/store/slices/data/action.test.ts +42 -0
- package/src/features/Conversation/store/slices/data/action.ts +3 -2
- package/src/features/Portal/GroupThread/Header/index.tsx +2 -2
- package/src/features/Portal/MessageDetail/Body/index.tsx +3 -3
- package/src/features/Portal/components/Header.tsx +3 -3
- package/src/features/ProfileEditor/AgentTool.tsx +50 -19
- package/src/hooks/useNavigateToAgent.ts +3 -3
- package/src/store/chat/slices/portal/action.test.ts +0 -41
- package/src/store/chat/slices/portal/action.ts +0 -25
- package/src/store/chat/slices/thread/action.test.ts +10 -6
- package/src/store/chat/slices/thread/action.ts +10 -3
- package/src/app/[variants]/(main)/group/features/Portal/features/Portal.tsx +0 -105
- package/src/app/[variants]/(main)/group/features/Portal/features/PortalPanel.tsx +0 -23
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.279](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.278...v2.0.0-next.279)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2026-01-13**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Fix new topic flick issue, fix thread portal not open correctly.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Fix new topic flick issue, closes [#11473](https://github.com/lobehub/lobe-chat/issues/11473) ([c53d372](https://github.com/lobehub/lobe-chat/commit/c53d372))
|
|
21
|
+
- **misc**: Fix thread portal not open correctly, closes [#11475](https://github.com/lobehub/lobe-chat/issues/11475) ([e6ff90b](https://github.com/lobehub/lobe-chat/commit/e6ff90b))
|
|
22
|
+
|
|
23
|
+
</details>
|
|
24
|
+
|
|
25
|
+
<div align="right">
|
|
26
|
+
|
|
27
|
+
[](#readme-top)
|
|
28
|
+
|
|
29
|
+
</div>
|
|
30
|
+
|
|
5
31
|
## [Version 2.0.0-next.278](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.277...v2.0.0-next.278)
|
|
6
32
|
|
|
7
33
|
<sup>Released on **2026-01-13**</sup>
|
package/changelog/v1.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.279",
|
|
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",
|
|
@@ -6,6 +6,7 @@ import { createStoreUpdater } from 'zustand-utils';
|
|
|
6
6
|
import { useFetchThreads } from '@/hooks/useFetchThreads';
|
|
7
7
|
import { useQueryState } from '@/hooks/useQueryParam';
|
|
8
8
|
import { useChatStore } from '@/store/chat';
|
|
9
|
+
import { PortalViewType } from '@/store/chat/slices/portal/initialState';
|
|
9
10
|
|
|
10
11
|
// sync outside state to useChatStore
|
|
11
12
|
const ThreadHydration = memo(() => {
|
|
@@ -31,7 +32,7 @@ const ThreadHydration = memo(() => {
|
|
|
31
32
|
// should open portal automatically when portalThread is set
|
|
32
33
|
useEffect(() => {
|
|
33
34
|
if (!!portalThread && !useChatStore.getState().showPortal) {
|
|
34
|
-
useChatStore.getState().
|
|
35
|
+
useChatStore.getState().pushPortalView({ threadId: portalThread, type: PortalViewType.Thread });
|
|
35
36
|
}
|
|
36
37
|
}, [portalThread]);
|
|
37
38
|
|
|
@@ -16,10 +16,10 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
18
|
const Layout = () => {
|
|
19
|
-
const [showMobilePortal, isPortalThread,
|
|
19
|
+
const [showMobilePortal, isPortalThread, clearPortalStack] = useChatStore((s) => [
|
|
20
20
|
s.showPortal,
|
|
21
21
|
portalThreadSelectors.showThread(s),
|
|
22
|
-
s.
|
|
22
|
+
s.clearPortalStack,
|
|
23
23
|
]);
|
|
24
24
|
const { t } = useTranslation('portal');
|
|
25
25
|
|
|
@@ -42,7 +42,7 @@ const Layout = () => {
|
|
|
42
42
|
destroyOnHidden
|
|
43
43
|
footer={null}
|
|
44
44
|
height={'95%'}
|
|
45
|
-
onCancel={() =>
|
|
45
|
+
onCancel={() => clearPortalStack()}
|
|
46
46
|
open={showMobilePortal}
|
|
47
47
|
styles={{
|
|
48
48
|
body: { padding: 0 },
|
|
@@ -11,6 +11,7 @@ import UserAvatar from '@/features/User/UserAvatar';
|
|
|
11
11
|
import { useAgentGroupStore } from '@/store/agentGroup';
|
|
12
12
|
import { agentGroupSelectors } from '@/store/agentGroup/selectors';
|
|
13
13
|
import { useChatStore } from '@/store/chat';
|
|
14
|
+
import { PortalViewType } from '@/store/chat/slices/portal/initialState';
|
|
14
15
|
import { useUserStore } from '@/store/user';
|
|
15
16
|
import { userProfileSelectors } from '@/store/user/slices/auth/selectors';
|
|
16
17
|
|
|
@@ -36,7 +37,7 @@ const GroupMember = memo<GroupMemberProps>(({ addModalOpen, onAddModalOpenChange
|
|
|
36
37
|
const addAgentsToGroup = useAgentGroupStore((s) => s.addAgentsToGroup);
|
|
37
38
|
const removeAgentFromGroup = useAgentGroupStore((s) => s.removeAgentFromGroup);
|
|
38
39
|
const toggleThread = useAgentGroupStore((s) => s.toggleThread);
|
|
39
|
-
const
|
|
40
|
+
const pushPortalView = useChatStore((s) => s.pushPortalView);
|
|
40
41
|
|
|
41
42
|
// Get members from store (excluding supervisor)
|
|
42
43
|
const groupMembers = useAgentGroupStore(agentGroupSelectors.getGroupMembers(groupId || ''));
|
|
@@ -76,7 +77,7 @@ const GroupMember = memo<GroupMemberProps>(({ addModalOpen, onAddModalOpenChange
|
|
|
76
77
|
|
|
77
78
|
const handleMemberClick = (agentId: string) => {
|
|
78
79
|
toggleThread(agentId);
|
|
79
|
-
|
|
80
|
+
pushPortalView({ agentId, type: PortalViewType.GroupThread });
|
|
80
81
|
};
|
|
81
82
|
|
|
82
83
|
return (
|
|
@@ -6,6 +6,7 @@ import { createStoreUpdater } from 'zustand-utils';
|
|
|
6
6
|
import { useFetchThreads } from '@/hooks/useFetchThreads';
|
|
7
7
|
import { useQueryState } from '@/hooks/useQueryParam';
|
|
8
8
|
import { useChatStore } from '@/store/chat';
|
|
9
|
+
import { PortalViewType } from '@/store/chat/slices/portal/initialState';
|
|
9
10
|
|
|
10
11
|
// sync outside state to useChatStore
|
|
11
12
|
const ThreadHydration = memo(() => {
|
|
@@ -31,7 +32,7 @@ const ThreadHydration = memo(() => {
|
|
|
31
32
|
// should open portal automatically when portalThread is set
|
|
32
33
|
useEffect(() => {
|
|
33
34
|
if (!!portalThread && !useChatStore.getState().showPortal) {
|
|
34
|
-
useChatStore.getState().
|
|
35
|
+
useChatStore.getState().pushPortalView({ threadId: portalThread, type: PortalViewType.Thread });
|
|
35
36
|
}
|
|
36
37
|
}, [portalThread]);
|
|
37
38
|
|
|
@@ -16,10 +16,10 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
18
|
const Layout = () => {
|
|
19
|
-
const [showMobilePortal, isPortalThread,
|
|
19
|
+
const [showMobilePortal, isPortalThread, clearPortalStack] = useChatStore((s) => [
|
|
20
20
|
s.showPortal,
|
|
21
21
|
portalThreadSelectors.showThread(s),
|
|
22
|
-
s.
|
|
22
|
+
s.clearPortalStack,
|
|
23
23
|
]);
|
|
24
24
|
const { t } = useTranslation('portal');
|
|
25
25
|
|
|
@@ -42,7 +42,7 @@ const Layout = () => {
|
|
|
42
42
|
destroyOnHidden
|
|
43
43
|
footer={null}
|
|
44
44
|
height={'95%'}
|
|
45
|
-
onCancel={() =>
|
|
45
|
+
onCancel={() => clearPortalStack()}
|
|
46
46
|
open={showMobilePortal}
|
|
47
47
|
styles={{
|
|
48
48
|
body: { padding: 0 },
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { AgentTool as SharedAgentTool } from '@/features/ProfileEditor';
|
|
4
|
+
import { useGroupProfileStore } from '@/store/groupProfile';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* AgentTool for group profile editor
|
|
7
8
|
* - Uses default settings (no web browsing, no filterAvailableInWeb, uses metaList)
|
|
9
|
+
* - Passes agentId from group profile store to display the correct member's plugins
|
|
8
10
|
*/
|
|
9
11
|
const AgentTool = () => {
|
|
10
|
-
|
|
12
|
+
const agentId = useGroupProfileStore((s) => s.activeTabId);
|
|
13
|
+
return <SharedAgentTool agentId={agentId} />;
|
|
11
14
|
};
|
|
12
15
|
|
|
13
16
|
export default AgentTool;
|
|
@@ -63,7 +63,11 @@ const ChatList = memo<ChatListProps>(({ disableActionsBar, welcome, itemContent
|
|
|
63
63
|
);
|
|
64
64
|
const messagesInit = useConversationStore(dataSelectors.messagesInit);
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
// When topicId is null (new conversation), show welcome directly without waiting for fetch
|
|
67
|
+
// because there's no server data to fetch - only local optimistic updates exist
|
|
68
|
+
const isNewConversation = !context.topicId;
|
|
69
|
+
|
|
70
|
+
if (!messagesInit && !isNewConversation) {
|
|
67
71
|
return <SkeletonList />;
|
|
68
72
|
}
|
|
69
73
|
|
|
@@ -505,6 +505,48 @@ describe('DataSlice', () => {
|
|
|
505
505
|
);
|
|
506
506
|
});
|
|
507
507
|
|
|
508
|
+
it('should not fetch when topicId is null (new conversation state)', () => {
|
|
509
|
+
const store = createStore({
|
|
510
|
+
context: { agentId: 'test-session', topicId: null, threadId: null },
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
store.getState().useFetchMessages({
|
|
514
|
+
agentId: 'test-session',
|
|
515
|
+
topicId: null,
|
|
516
|
+
threadId: null,
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// SWR should be called with null key when topicId is null
|
|
520
|
+
// This prevents fetching empty data that would overwrite local optimistic updates
|
|
521
|
+
expect(vi.mocked(useClientDataSWRWithSync)).toHaveBeenCalledWith(
|
|
522
|
+
null,
|
|
523
|
+
expect.any(Function),
|
|
524
|
+
expect.any(Object),
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
// messageService.getMessages should NOT be called
|
|
528
|
+
expect(messageService.getMessages).not.toHaveBeenCalled();
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('should not fetch when topicId is undefined (new conversation state)', () => {
|
|
532
|
+
const store = createStore({
|
|
533
|
+
context: { agentId: 'test-session', topicId: null, threadId: null },
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
store.getState().useFetchMessages({
|
|
537
|
+
agentId: 'test-session',
|
|
538
|
+
topicId: undefined as any,
|
|
539
|
+
threadId: null,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// SWR should be called with null key when topicId is undefined
|
|
543
|
+
expect(vi.mocked(useClientDataSWRWithSync)).toHaveBeenCalledWith(
|
|
544
|
+
null,
|
|
545
|
+
expect.any(Function),
|
|
546
|
+
expect.any(Object),
|
|
547
|
+
);
|
|
548
|
+
});
|
|
549
|
+
|
|
508
550
|
it('should use different SWR keys for different threadIds', () => {
|
|
509
551
|
const store1 = createStore({
|
|
510
552
|
context: { agentId: 'session-1', topicId: 'topic-1', threadId: 'thread-1' },
|
|
@@ -103,8 +103,9 @@ export const dataSlice: StateCreator<
|
|
|
103
103
|
useFetchMessages: (context, skipFetch) => {
|
|
104
104
|
// When skipFetch is true, SWR key is null - no fetch occurs
|
|
105
105
|
// This is used when external messages are provided (e.g., creating new thread)
|
|
106
|
-
//
|
|
107
|
-
|
|
106
|
+
// Also skip fetch when topicId is null (new conversation state) - there's no server data,
|
|
107
|
+
// only local optimistic updates. Fetching would return empty array and overwrite local data.
|
|
108
|
+
const shouldFetch = !skipFetch && !!context.agentId && !!context.topicId;
|
|
108
109
|
|
|
109
110
|
return useClientDataSWRWithSync<UIChatMessage[]>(
|
|
110
111
|
shouldFetch ? ['CONVERSATION_FETCH_MESSAGES', context] : null,
|
|
@@ -12,10 +12,10 @@ import { useSessionStore } from '@/store/session';
|
|
|
12
12
|
import { sessionSelectors } from '@/store/session/selectors';
|
|
13
13
|
|
|
14
14
|
const Header = memo(() => {
|
|
15
|
-
const
|
|
15
|
+
const clearPortalStack = useChatStore((s) => s.clearPortalStack);
|
|
16
16
|
const close = () => {
|
|
17
17
|
useAgentGroupStore.setState({ activeThreadAgentId: '' });
|
|
18
|
-
|
|
18
|
+
clearPortalStack();
|
|
19
19
|
};
|
|
20
20
|
const activeThreadAgentId = useAgentGroupStore((s) => s.activeThreadAgentId);
|
|
21
21
|
|
|
@@ -15,9 +15,9 @@ const md = css`
|
|
|
15
15
|
`;
|
|
16
16
|
|
|
17
17
|
const MessageDetailBody = () => {
|
|
18
|
-
const [messageDetailId,
|
|
18
|
+
const [messageDetailId, clearPortalStack] = useChatStore((s) => [
|
|
19
19
|
chatPortalSelectors.messageDetailId(s),
|
|
20
|
-
s.
|
|
20
|
+
s.clearPortalStack,
|
|
21
21
|
]);
|
|
22
22
|
|
|
23
23
|
const message = useChatStore(dbMessageSelectors.getDbMessageById(messageDetailId || ''), isEqual);
|
|
@@ -26,7 +26,7 @@ const MessageDetailBody = () => {
|
|
|
26
26
|
|
|
27
27
|
useEffect(() => {
|
|
28
28
|
if (!message) {
|
|
29
|
-
|
|
29
|
+
clearPortalStack();
|
|
30
30
|
}
|
|
31
31
|
}, [message]);
|
|
32
32
|
|
|
@@ -10,10 +10,10 @@ import { useChatStore } from '@/store/chat';
|
|
|
10
10
|
import { chatPortalSelectors } from '@/store/chat/selectors';
|
|
11
11
|
|
|
12
12
|
const Header = memo<{ title: ReactNode }>(({ title }) => {
|
|
13
|
-
const [canGoBack, goBack,
|
|
13
|
+
const [canGoBack, goBack, clearPortalStack] = useChatStore((s) => [
|
|
14
14
|
chatPortalSelectors.canGoBack(s),
|
|
15
15
|
s.goBack,
|
|
16
|
-
s.
|
|
16
|
+
s.clearPortalStack,
|
|
17
17
|
]);
|
|
18
18
|
|
|
19
19
|
return (
|
|
@@ -30,7 +30,7 @@ const Header = memo<{ title: ReactNode }>(({ title }) => {
|
|
|
30
30
|
<ActionIcon
|
|
31
31
|
icon={PanelRightCloseIcon}
|
|
32
32
|
onClick={() => {
|
|
33
|
-
|
|
33
|
+
clearPortalStack();
|
|
34
34
|
}}
|
|
35
35
|
size={DESKTOP_HEADER_ICON_SIZE}
|
|
36
36
|
/>
|
|
@@ -16,7 +16,7 @@ import PluginStore from '@/features/PluginStore';
|
|
|
16
16
|
import { useCheckPluginsIsInstalled } from '@/hooks/useCheckPluginsIsInstalled';
|
|
17
17
|
import { useFetchInstalledPlugins } from '@/hooks/useFetchInstalledPlugins';
|
|
18
18
|
import { useAgentStore } from '@/store/agent';
|
|
19
|
-
import {
|
|
19
|
+
import { agentSelectors, chatConfigByIdSelectors } from '@/store/agent/selectors';
|
|
20
20
|
import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
|
|
21
21
|
import { useToolStore } from '@/store/tool';
|
|
22
22
|
import {
|
|
@@ -82,6 +82,11 @@ const KlavisIcon = memo<Pick<KlavisServerType, 'icon' | 'label'>>(({ icon, label
|
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
export interface AgentToolProps {
|
|
85
|
+
/**
|
|
86
|
+
* Optional agent ID to use instead of currentAgentConfig
|
|
87
|
+
* Used in group profile to specify which member's plugins to display
|
|
88
|
+
*/
|
|
89
|
+
agentId?: string;
|
|
85
90
|
/**
|
|
86
91
|
* Whether to filter tools by availableInWeb property
|
|
87
92
|
* @default false
|
|
@@ -100,15 +105,17 @@ export interface AgentToolProps {
|
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
const AgentTool = memo<AgentToolProps>(
|
|
103
|
-
({ showWebBrowsing = false, filterAvailableInWeb = false, useAllMetaList = false }) => {
|
|
108
|
+
({ agentId, showWebBrowsing = false, filterAvailableInWeb = false, useAllMetaList = false }) => {
|
|
104
109
|
const { t } = useTranslation('setting');
|
|
105
|
-
const
|
|
110
|
+
const activeAgentId = useAgentStore((s) => s.activeAgentId);
|
|
111
|
+
const effectiveAgentId = agentId || activeAgentId || '';
|
|
112
|
+
const config = useAgentStore(agentSelectors.getAgentConfigById(effectiveAgentId), isEqual);
|
|
106
113
|
|
|
107
114
|
// Plugin state management
|
|
108
115
|
const plugins = config?.plugins || [];
|
|
109
116
|
|
|
110
|
-
const
|
|
111
|
-
const
|
|
117
|
+
const updateAgentConfigById = useAgentStore((s) => s.updateAgentConfigById);
|
|
118
|
+
const updateAgentChatConfigById = useAgentStore((s) => s.updateAgentChatConfigById);
|
|
112
119
|
const installedPluginList = useToolStore(pluginSelectors.installedPluginMetaList, isEqual);
|
|
113
120
|
|
|
114
121
|
// Use appropriate builtin list based on prop
|
|
@@ -117,8 +124,8 @@ const AgentTool = memo<AgentToolProps>(
|
|
|
117
124
|
isEqual,
|
|
118
125
|
);
|
|
119
126
|
|
|
120
|
-
// Web browsing uses searchMode instead of plugins array
|
|
121
|
-
const isSearchEnabled = useAgentStore(
|
|
127
|
+
// Web browsing uses searchMode instead of plugins array - use byId selector
|
|
128
|
+
const isSearchEnabled = useAgentStore(chatConfigByIdSelectors.isEnableSearchById(effectiveAgentId));
|
|
122
129
|
|
|
123
130
|
// Klavis 相关状态
|
|
124
131
|
const allKlavisServers = useToolStore(klavisStoreSelectors.getServers, isEqual);
|
|
@@ -144,11 +151,34 @@ const AgentTool = memo<AgentToolProps>(
|
|
|
144
151
|
// 使用 SWR 加载用户的 Klavis 集成(从数据库)
|
|
145
152
|
useFetchUserKlavisServers(isKlavisEnabledInEnv);
|
|
146
153
|
|
|
147
|
-
// Toggle web browsing via searchMode
|
|
154
|
+
// Toggle web browsing via searchMode - use byId action
|
|
148
155
|
const toggleWebBrowsing = useCallback(async () => {
|
|
156
|
+
if (!effectiveAgentId) return;
|
|
149
157
|
const nextMode = isSearchEnabled ? 'off' : 'auto';
|
|
150
|
-
await
|
|
151
|
-
}, [isSearchEnabled,
|
|
158
|
+
await updateAgentChatConfigById(effectiveAgentId, { searchMode: nextMode });
|
|
159
|
+
}, [isSearchEnabled, updateAgentChatConfigById, effectiveAgentId]);
|
|
160
|
+
|
|
161
|
+
// Toggle a plugin - use byId action
|
|
162
|
+
const togglePlugin = useCallback(
|
|
163
|
+
async (pluginId: string, state?: boolean) => {
|
|
164
|
+
if (!effectiveAgentId) return;
|
|
165
|
+
const currentPlugins = plugins;
|
|
166
|
+
const hasPlugin = currentPlugins.includes(pluginId);
|
|
167
|
+
const shouldEnable = state !== undefined ? state : !hasPlugin;
|
|
168
|
+
|
|
169
|
+
let newPlugins: string[];
|
|
170
|
+
if (shouldEnable && !hasPlugin) {
|
|
171
|
+
newPlugins = [...currentPlugins, pluginId];
|
|
172
|
+
} else if (!shouldEnable && hasPlugin) {
|
|
173
|
+
newPlugins = currentPlugins.filter((id) => id !== pluginId);
|
|
174
|
+
} else {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await updateAgentConfigById(effectiveAgentId, { plugins: newPlugins });
|
|
179
|
+
},
|
|
180
|
+
[effectiveAgentId, plugins, updateAgentConfigById],
|
|
181
|
+
);
|
|
152
182
|
|
|
153
183
|
// Check if a tool is enabled (handles web browsing specially)
|
|
154
184
|
const isToolEnabled = useCallback(
|
|
@@ -167,10 +197,10 @@ const AgentTool = memo<AgentToolProps>(
|
|
|
167
197
|
if (showWebBrowsing && identifier === WEB_BROWSING_IDENTIFIER) {
|
|
168
198
|
await toggleWebBrowsing();
|
|
169
199
|
} else {
|
|
170
|
-
await
|
|
200
|
+
await togglePlugin(identifier);
|
|
171
201
|
}
|
|
172
202
|
},
|
|
173
|
-
[toggleWebBrowsing,
|
|
203
|
+
[toggleWebBrowsing, togglePlugin, showWebBrowsing],
|
|
174
204
|
);
|
|
175
205
|
|
|
176
206
|
// Set default tab based on installed plugins (only on first load)
|
|
@@ -240,7 +270,7 @@ const AgentTool = memo<AgentToolProps>(
|
|
|
240
270
|
[isKlavisEnabledInEnv, allKlavisServers],
|
|
241
271
|
);
|
|
242
272
|
|
|
243
|
-
// Handle plugin remove via Tag close
|
|
273
|
+
// Handle plugin remove via Tag close - use byId actions
|
|
244
274
|
const handleRemovePlugin =
|
|
245
275
|
(
|
|
246
276
|
pluginId: string | { enabled: boolean; identifier: string; settings: Record<string, any> },
|
|
@@ -250,9 +280,10 @@ const AgentTool = memo<AgentToolProps>(
|
|
|
250
280
|
e.stopPropagation();
|
|
251
281
|
const identifier = typeof pluginId === 'string' ? pluginId : pluginId?.identifier;
|
|
252
282
|
if (showWebBrowsing && identifier === WEB_BROWSING_IDENTIFIER) {
|
|
253
|
-
|
|
283
|
+
if (!effectiveAgentId) return;
|
|
284
|
+
await updateAgentChatConfigById(effectiveAgentId, { searchMode: 'off' });
|
|
254
285
|
} else {
|
|
255
|
-
|
|
286
|
+
await togglePlugin(identifier, false);
|
|
256
287
|
}
|
|
257
288
|
};
|
|
258
289
|
|
|
@@ -304,13 +335,13 @@ const AgentTool = memo<AgentToolProps>(
|
|
|
304
335
|
label={item.title}
|
|
305
336
|
onUpdate={async () => {
|
|
306
337
|
setUpdating(true);
|
|
307
|
-
await
|
|
338
|
+
await togglePlugin(item.identifier);
|
|
308
339
|
setUpdating(false);
|
|
309
340
|
}}
|
|
310
341
|
/>
|
|
311
342
|
),
|
|
312
343
|
})),
|
|
313
|
-
[installedPluginList, plugins,
|
|
344
|
+
[installedPluginList, plugins, togglePlugin],
|
|
314
345
|
);
|
|
315
346
|
|
|
316
347
|
// All tab items (市场 tab)
|
|
@@ -411,7 +442,7 @@ const AgentTool = memo<AgentToolProps>(
|
|
|
411
442
|
label={item.title}
|
|
412
443
|
onUpdate={async () => {
|
|
413
444
|
setUpdating(true);
|
|
414
|
-
await
|
|
445
|
+
await togglePlugin(item.identifier);
|
|
415
446
|
setUpdating(false);
|
|
416
447
|
}}
|
|
417
448
|
/>
|
|
@@ -435,7 +466,7 @@ const AgentTool = memo<AgentToolProps>(
|
|
|
435
466
|
plugins,
|
|
436
467
|
isToolEnabled,
|
|
437
468
|
handleToggleTool,
|
|
438
|
-
|
|
469
|
+
togglePlugin,
|
|
439
470
|
t,
|
|
440
471
|
]);
|
|
441
472
|
|
|
@@ -5,15 +5,15 @@ import { useQueryRoute } from '@/hooks/useQueryRoute';
|
|
|
5
5
|
import { useChatStore } from '@/store/chat';
|
|
6
6
|
|
|
7
7
|
export const useNavigateToAgent = () => {
|
|
8
|
-
const
|
|
8
|
+
const clearPortalStack = useChatStore((s) => s.clearPortalStack);
|
|
9
9
|
const router = useQueryRoute();
|
|
10
10
|
|
|
11
11
|
return useCallback(
|
|
12
12
|
(agentId: string) => {
|
|
13
|
-
|
|
13
|
+
clearPortalStack();
|
|
14
14
|
|
|
15
15
|
router.push(SESSION_CHAT_URL(agentId, false));
|
|
16
16
|
},
|
|
17
|
-
[
|
|
17
|
+
[clearPortalStack, router],
|
|
18
18
|
);
|
|
19
19
|
};
|
|
@@ -268,45 +268,4 @@ describe('chatDockSlice', () => {
|
|
|
268
268
|
});
|
|
269
269
|
});
|
|
270
270
|
|
|
271
|
-
describe('toggleDock', () => {
|
|
272
|
-
it('should toggle dock state when no argument is provided', () => {
|
|
273
|
-
const { result } = renderHook(() => useChatStore());
|
|
274
|
-
|
|
275
|
-
expect(result.current.showPortal).toBe(false);
|
|
276
|
-
|
|
277
|
-
act(() => {
|
|
278
|
-
result.current.togglePortal();
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
expect(result.current.showPortal).toBe(true);
|
|
282
|
-
|
|
283
|
-
act(() => {
|
|
284
|
-
result.current.togglePortal();
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
expect(result.current.showPortal).toBe(false);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it('should set dock state to the provided value', () => {
|
|
291
|
-
const { result } = renderHook(() => useChatStore());
|
|
292
|
-
|
|
293
|
-
act(() => {
|
|
294
|
-
result.current.togglePortal(true);
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
expect(result.current.showPortal).toBe(true);
|
|
298
|
-
|
|
299
|
-
act(() => {
|
|
300
|
-
result.current.togglePortal(false);
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
expect(result.current.showPortal).toBe(false);
|
|
304
|
-
|
|
305
|
-
act(() => {
|
|
306
|
-
result.current.togglePortal(true);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
expect(result.current.showPortal).toBe(true);
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
271
|
});
|
|
@@ -28,7 +28,6 @@ export interface ChatPortalAction {
|
|
|
28
28
|
pushPortalView: (view: PortalViewData) => void;
|
|
29
29
|
replacePortalView: (view: PortalViewData) => void;
|
|
30
30
|
toggleNotebook: (open?: boolean) => void;
|
|
31
|
-
togglePortal: (open?: boolean) => void;
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
// Helper to get current view type from stack
|
|
@@ -222,28 +221,4 @@ pushPortalView: (view) => {
|
|
|
222
221
|
get().closeNotebook();
|
|
223
222
|
}
|
|
224
223
|
},
|
|
225
|
-
|
|
226
|
-
togglePortal: (open) => {
|
|
227
|
-
const nextOpen = open === undefined ? !get().showPortal : open;
|
|
228
|
-
|
|
229
|
-
if (!nextOpen) {
|
|
230
|
-
// When closing, clear the stack
|
|
231
|
-
set({ portalStack: [], showPortal: false }, false, 'togglePortal/close');
|
|
232
|
-
} else {
|
|
233
|
-
// When opening, if stack is empty, push Home view
|
|
234
|
-
const { portalStack } = get();
|
|
235
|
-
if (portalStack.length === 0) {
|
|
236
|
-
set(
|
|
237
|
-
{
|
|
238
|
-
portalStack: [{ type: PortalViewType.Home }],
|
|
239
|
-
showPortal: true,
|
|
240
|
-
},
|
|
241
|
-
false,
|
|
242
|
-
'togglePortal/openHome',
|
|
243
|
-
);
|
|
244
|
-
} else {
|
|
245
|
-
set({ showPortal: true }, false, 'togglePortal/open');
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
},
|
|
249
224
|
});
|
|
@@ -141,7 +141,7 @@ describe('thread action', () => {
|
|
|
141
141
|
describe('openThreadCreator', () => {
|
|
142
142
|
it('should set thread creator state and open portal', () => {
|
|
143
143
|
const { result } = renderHook(() => useChatStore());
|
|
144
|
-
const
|
|
144
|
+
const pushPortalViewSpy = vi.spyOn(result.current, 'pushPortalView');
|
|
145
145
|
|
|
146
146
|
act(() => {
|
|
147
147
|
result.current.openThreadCreator('message-id');
|
|
@@ -150,14 +150,14 @@ describe('thread action', () => {
|
|
|
150
150
|
expect(result.current.threadStartMessageId).toBe('message-id');
|
|
151
151
|
expect(result.current.portalThreadId).toBeUndefined();
|
|
152
152
|
expect(result.current.startToForkThread).toBe(true);
|
|
153
|
-
expect(
|
|
153
|
+
expect(pushPortalViewSpy).toHaveBeenCalledWith({ type: 'thread', startMessageId: 'message-id' });
|
|
154
154
|
});
|
|
155
155
|
});
|
|
156
156
|
|
|
157
157
|
describe('openThreadInPortal', () => {
|
|
158
158
|
it('should set portal thread state and open portal', () => {
|
|
159
159
|
const { result } = renderHook(() => useChatStore());
|
|
160
|
-
const
|
|
160
|
+
const pushPortalViewSpy = vi.spyOn(result.current, 'pushPortalView');
|
|
161
161
|
|
|
162
162
|
act(() => {
|
|
163
163
|
result.current.openThreadInPortal('thread-id', 'source-message-id');
|
|
@@ -166,7 +166,11 @@ describe('thread action', () => {
|
|
|
166
166
|
expect(result.current.portalThreadId).toBe('thread-id');
|
|
167
167
|
expect(result.current.threadStartMessageId).toBe('source-message-id');
|
|
168
168
|
expect(result.current.startToForkThread).toBe(false);
|
|
169
|
-
expect(
|
|
169
|
+
expect(pushPortalViewSpy).toHaveBeenCalledWith({
|
|
170
|
+
type: 'thread',
|
|
171
|
+
threadId: 'thread-id',
|
|
172
|
+
startMessageId: 'source-message-id',
|
|
173
|
+
});
|
|
170
174
|
});
|
|
171
175
|
});
|
|
172
176
|
|
|
@@ -182,7 +186,7 @@ describe('thread action', () => {
|
|
|
182
186
|
});
|
|
183
187
|
});
|
|
184
188
|
|
|
185
|
-
const
|
|
189
|
+
const clearPortalStackSpy = vi.spyOn(result.current, 'clearPortalStack');
|
|
186
190
|
|
|
187
191
|
act(() => {
|
|
188
192
|
result.current.closeThreadPortal();
|
|
@@ -191,7 +195,7 @@ describe('thread action', () => {
|
|
|
191
195
|
expect(result.current.portalThreadId).toBeUndefined();
|
|
192
196
|
expect(result.current.threadStartMessageId).toBeUndefined();
|
|
193
197
|
expect(result.current.startToForkThread).toBeUndefined();
|
|
194
|
-
expect(
|
|
198
|
+
expect(clearPortalStackSpy).toHaveBeenCalled();
|
|
195
199
|
});
|
|
196
200
|
});
|
|
197
201
|
|
|
@@ -19,6 +19,7 @@ import { merge } from '@/utils/merge';
|
|
|
19
19
|
import { setNamespace } from '@/utils/storeDebug';
|
|
20
20
|
|
|
21
21
|
import { displayMessageSelectors } from '../message/selectors';
|
|
22
|
+
import { PortalViewType } from '../portal/initialState';
|
|
22
23
|
import { type ThreadDispatch, threadReducer } from './reducer';
|
|
23
24
|
import { genParentMessages } from './selectors';
|
|
24
25
|
|
|
@@ -88,7 +89,8 @@ export const chatThreadMessage: StateCreator<
|
|
|
88
89
|
false,
|
|
89
90
|
'openThreadCreator',
|
|
90
91
|
);
|
|
91
|
-
|
|
92
|
+
// Push Thread view to portal stack instead of togglePortal
|
|
93
|
+
get().pushPortalView({ type: PortalViewType.Thread, startMessageId: messageId });
|
|
92
94
|
},
|
|
93
95
|
openThreadInPortal: (threadId, sourceMessageId) => {
|
|
94
96
|
set(
|
|
@@ -96,7 +98,12 @@ export const chatThreadMessage: StateCreator<
|
|
|
96
98
|
false,
|
|
97
99
|
'openThreadInPortal',
|
|
98
100
|
);
|
|
99
|
-
|
|
101
|
+
// Push Thread view to portal stack with threadId
|
|
102
|
+
get().pushPortalView({
|
|
103
|
+
type: PortalViewType.Thread,
|
|
104
|
+
threadId,
|
|
105
|
+
startMessageId: sourceMessageId ?? undefined,
|
|
106
|
+
});
|
|
100
107
|
},
|
|
101
108
|
|
|
102
109
|
closeThreadPortal: () => {
|
|
@@ -105,7 +112,7 @@ export const chatThreadMessage: StateCreator<
|
|
|
105
112
|
false,
|
|
106
113
|
'closeThreadPortal',
|
|
107
114
|
);
|
|
108
|
-
get().
|
|
115
|
+
get().clearPortalStack();
|
|
109
116
|
},
|
|
110
117
|
createThread: async ({ message, sourceMessageId, topicId, type }) => {
|
|
111
118
|
set({ isCreatingThread: true }, false, n('creatingThread/start'));
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { DraggablePanel, DraggablePanelContainer, type DraggablePanelProps } from '@lobehub/ui';
|
|
4
|
-
import { Flexbox } from '@lobehub/ui';
|
|
5
|
-
import { createStaticStyles, useResponsive } from 'antd-style';
|
|
6
|
-
import isEqual from 'fast-deep-equal';
|
|
7
|
-
import { Activity, type PropsWithChildren, memo, useState } from 'react';
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
CHAT_PORTAL_MAX_WIDTH,
|
|
11
|
-
CHAT_PORTAL_TOOL_UI_WIDTH,
|
|
12
|
-
CHAT_PORTAL_WIDTH,
|
|
13
|
-
} from '@/const/layoutTokens';
|
|
14
|
-
import { useChatStore } from '@/store/chat';
|
|
15
|
-
import { chatPortalSelectors, portalThreadSelectors } from '@/store/chat/selectors';
|
|
16
|
-
import { useGlobalStore } from '@/store/global';
|
|
17
|
-
import { systemStatusSelectors } from '@/store/global/selectors';
|
|
18
|
-
|
|
19
|
-
const styles = createStaticStyles(({ css, cssVar }) => ({
|
|
20
|
-
content: css`
|
|
21
|
-
display: flex;
|
|
22
|
-
flex-direction: column;
|
|
23
|
-
height: 100% !important;
|
|
24
|
-
`,
|
|
25
|
-
drawer: css`
|
|
26
|
-
z-index: 10;
|
|
27
|
-
height: 100%;
|
|
28
|
-
background: ${cssVar.colorBgContainer};
|
|
29
|
-
`,
|
|
30
|
-
panel: css`
|
|
31
|
-
overflow: hidden;
|
|
32
|
-
height: 100%;
|
|
33
|
-
background: ${cssVar.colorBgContainer};
|
|
34
|
-
`,
|
|
35
|
-
}));
|
|
36
|
-
|
|
37
|
-
const PortalPanel = memo(({ children }: PropsWithChildren) => {
|
|
38
|
-
const { md = true } = useResponsive();
|
|
39
|
-
|
|
40
|
-
const [showPortal, showToolUI, showArtifactUI, showThread] = useChatStore((s) => [
|
|
41
|
-
chatPortalSelectors.showPortal(s),
|
|
42
|
-
chatPortalSelectors.showPluginUI(s),
|
|
43
|
-
chatPortalSelectors.showArtifactUI(s),
|
|
44
|
-
portalThreadSelectors.showThread(s),
|
|
45
|
-
]);
|
|
46
|
-
|
|
47
|
-
const [portalWidth, updateSystemStatus] = useGlobalStore((s) => [
|
|
48
|
-
systemStatusSelectors.portalWidth(s),
|
|
49
|
-
s.updateSystemStatus,
|
|
50
|
-
]);
|
|
51
|
-
|
|
52
|
-
const [tmpWidth, setWidth] = useState(portalWidth);
|
|
53
|
-
if (tmpWidth !== portalWidth) setWidth(portalWidth);
|
|
54
|
-
|
|
55
|
-
const handleSizeChange: DraggablePanelProps['onSizeChange'] = (_, size) => {
|
|
56
|
-
if (!size) return;
|
|
57
|
-
const nextWidth = typeof size.width === 'string' ? Number.parseInt(size.width) : size.width;
|
|
58
|
-
if (!nextWidth) return;
|
|
59
|
-
|
|
60
|
-
if (isEqual(nextWidth, portalWidth)) return;
|
|
61
|
-
setWidth(nextWidth);
|
|
62
|
-
updateSystemStatus({ portalWidth: nextWidth });
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
return (
|
|
66
|
-
<DraggablePanel
|
|
67
|
-
className={styles.drawer}
|
|
68
|
-
classNames={{
|
|
69
|
-
content: styles.content,
|
|
70
|
-
}}
|
|
71
|
-
defaultSize={{ width: tmpWidth }}
|
|
72
|
-
expand={showPortal}
|
|
73
|
-
maxWidth={CHAT_PORTAL_MAX_WIDTH}
|
|
74
|
-
minWidth={
|
|
75
|
-
(showArtifactUI || showToolUI || showThread) && md
|
|
76
|
-
? CHAT_PORTAL_TOOL_UI_WIDTH
|
|
77
|
-
: CHAT_PORTAL_WIDTH
|
|
78
|
-
}
|
|
79
|
-
mode={md ? 'fixed' : 'float'}
|
|
80
|
-
onSizeChange={handleSizeChange}
|
|
81
|
-
placement={'right'}
|
|
82
|
-
showHandleWhenCollapsed={false}
|
|
83
|
-
showHandleWideArea={false}
|
|
84
|
-
size={{ height: '100%', width: portalWidth }}
|
|
85
|
-
styles={{
|
|
86
|
-
handle: { display: 'none' },
|
|
87
|
-
}}
|
|
88
|
-
>
|
|
89
|
-
<DraggablePanelContainer
|
|
90
|
-
style={{
|
|
91
|
-
flex: 'none',
|
|
92
|
-
height: '100%',
|
|
93
|
-
maxHeight: '100vh',
|
|
94
|
-
minWidth: CHAT_PORTAL_WIDTH,
|
|
95
|
-
}}
|
|
96
|
-
>
|
|
97
|
-
<Activity mode={showPortal ? 'visible' : 'hidden'} name="GroupPortal">
|
|
98
|
-
<Flexbox className={styles.panel}>{children}</Flexbox>
|
|
99
|
-
</Activity>
|
|
100
|
-
</DraggablePanelContainer>
|
|
101
|
-
</DraggablePanel>
|
|
102
|
-
);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
export default PortalPanel;
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { Suspense, memo } from 'react';
|
|
2
|
-
|
|
3
|
-
import DesktopLayout from '@/app/[variants]/(main)/group/features/Portal/_layout/Desktop';
|
|
4
|
-
import MobileLayout from '@/app/[variants]/(main)/group/features/Portal/_layout/Mobile';
|
|
5
|
-
import Loading from '@/components/Loading/BrandTextLoading';
|
|
6
|
-
|
|
7
|
-
interface PortalPanelProps {
|
|
8
|
-
mobile?: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const PortalPanel = memo<PortalPanelProps>(({ mobile }) => {
|
|
12
|
-
const Layout = mobile ? MobileLayout : DesktopLayout;
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<Suspense fallback={<Loading debugId="PortalPanel" />}>
|
|
16
|
-
<Layout />
|
|
17
|
-
</Suspense>
|
|
18
|
-
);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
PortalPanel.displayName = 'PortalPanel';
|
|
22
|
-
|
|
23
|
-
export default PortalPanel;
|