@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.
Files changed (23) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/changelog/v1.json +9 -0
  3. package/package.json +1 -1
  4. package/src/app/[variants]/(main)/agent/features/Conversation/ThreadHydration.tsx +2 -1
  5. package/src/app/[variants]/(main)/agent/features/Portal/_layout/Mobile.tsx +3 -3
  6. package/src/app/[variants]/(main)/group/_layout/Sidebar/GroupConfig/GroupMember.tsx +3 -2
  7. package/src/app/[variants]/(main)/group/features/Conversation/ThreadHydration.tsx +2 -1
  8. package/src/app/[variants]/(main)/group/features/Portal/_layout/Mobile.tsx +3 -3
  9. package/src/app/[variants]/(main)/group/profile/features/MemberProfile/AgentTool.tsx +4 -1
  10. package/src/features/Conversation/ChatList/index.tsx +5 -1
  11. package/src/features/Conversation/store/slices/data/action.test.ts +42 -0
  12. package/src/features/Conversation/store/slices/data/action.ts +3 -2
  13. package/src/features/Portal/GroupThread/Header/index.tsx +2 -2
  14. package/src/features/Portal/MessageDetail/Body/index.tsx +3 -3
  15. package/src/features/Portal/components/Header.tsx +3 -3
  16. package/src/features/ProfileEditor/AgentTool.tsx +50 -19
  17. package/src/hooks/useNavigateToAgent.ts +3 -3
  18. package/src/store/chat/slices/portal/action.test.ts +0 -41
  19. package/src/store/chat/slices/portal/action.ts +0 -25
  20. package/src/store/chat/slices/thread/action.test.ts +10 -6
  21. package/src/store/chat/slices/thread/action.ts +10 -3
  22. package/src/app/[variants]/(main)/group/features/Portal/features/Portal.tsx +0 -105
  23. 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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#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
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Fix new topic flick issue, fix thread portal not open correctly."
6
+ ]
7
+ },
8
+ "date": "2026-01-13",
9
+ "version": "2.0.0-next.279"
10
+ },
2
11
  {
3
12
  "children": {},
4
13
  "date": "2026-01-13",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.278",
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().togglePortal(true);
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, togglePortal] = useChatStore((s) => [
19
+ const [showMobilePortal, isPortalThread, clearPortalStack] = useChatStore((s) => [
20
20
  s.showPortal,
21
21
  portalThreadSelectors.showThread(s),
22
- s.togglePortal,
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={() => togglePortal(false)}
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 togglePortal = useChatStore((s) => s.togglePortal);
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
- togglePortal(true);
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().togglePortal(true);
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, togglePortal] = useChatStore((s) => [
19
+ const [showMobilePortal, isPortalThread, clearPortalStack] = useChatStore((s) => [
20
20
  s.showPortal,
21
21
  portalThreadSelectors.showThread(s),
22
- s.togglePortal,
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={() => togglePortal(false)}
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
- return <SharedAgentTool />;
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
- if (!messagesInit) {
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
- // Allow fetch if: has agentId (both agent topics and group topics have agentId)
107
- const shouldFetch = !skipFetch && !!context.agentId;
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 togglePortal = useChatStore((s) => s.togglePortal);
15
+ const clearPortalStack = useChatStore((s) => s.clearPortalStack);
16
16
  const close = () => {
17
17
  useAgentGroupStore.setState({ activeThreadAgentId: '' });
18
- togglePortal(false);
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, togglePortal] = useChatStore((s) => [
18
+ const [messageDetailId, clearPortalStack] = useChatStore((s) => [
19
19
  chatPortalSelectors.messageDetailId(s),
20
- s.togglePortal,
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
- togglePortal(false);
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, togglePortal] = useChatStore((s) => [
13
+ const [canGoBack, goBack, clearPortalStack] = useChatStore((s) => [
14
14
  chatPortalSelectors.canGoBack(s),
15
15
  s.goBack,
16
- s.togglePortal,
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
- togglePortal(false);
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 { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
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 config = useAgentStore(agentSelectors.currentAgentConfig, isEqual);
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 toggleAgentPlugin = useAgentStore((s) => s.toggleAgentPlugin);
111
- const updateAgentChatConfig = useAgentStore((s) => s.updateAgentChatConfig);
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(agentChatConfigSelectors.isAgentEnableSearch);
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 updateAgentChatConfig({ searchMode: nextMode });
151
- }, [isSearchEnabled, updateAgentChatConfig]);
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 toggleAgentPlugin(identifier);
200
+ await togglePlugin(identifier);
171
201
  }
172
202
  },
173
- [toggleWebBrowsing, toggleAgentPlugin, showWebBrowsing],
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
- await updateAgentChatConfig({ searchMode: 'off' });
283
+ if (!effectiveAgentId) return;
284
+ await updateAgentChatConfigById(effectiveAgentId, { searchMode: 'off' });
254
285
  } else {
255
- toggleAgentPlugin(identifier, false);
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 toggleAgentPlugin(item.identifier);
338
+ await togglePlugin(item.identifier);
308
339
  setUpdating(false);
309
340
  }}
310
341
  />
311
342
  ),
312
343
  })),
313
- [installedPluginList, plugins, toggleAgentPlugin],
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 toggleAgentPlugin(item.identifier);
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
- toggleAgentPlugin,
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 togglePortal = useChatStore((s) => s.togglePortal);
8
+ const clearPortalStack = useChatStore((s) => s.clearPortalStack);
9
9
  const router = useQueryRoute();
10
10
 
11
11
  return useCallback(
12
12
  (agentId: string) => {
13
- togglePortal(false);
13
+ clearPortalStack();
14
14
 
15
15
  router.push(SESSION_CHAT_URL(agentId, false));
16
16
  },
17
- [togglePortal, router],
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 togglePortalSpy = vi.spyOn(result.current, 'togglePortal');
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(togglePortalSpy).toHaveBeenCalledWith(true);
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 togglePortalSpy = vi.spyOn(result.current, 'togglePortal');
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(togglePortalSpy).toHaveBeenCalledWith(true);
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 togglePortalSpy = vi.spyOn(result.current, 'togglePortal');
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(togglePortalSpy).toHaveBeenCalledWith(false);
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
- get().togglePortal(true);
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
- get().togglePortal(true);
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().togglePortal(false);
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;