@lobehub/lobehub 2.0.0-next.305 → 2.0.0-next.307

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 (80) hide show
  1. package/.vscode/settings.json +18 -3
  2. package/CHANGELOG.md +53 -0
  3. package/changelog/v1.json +18 -0
  4. package/e2e/src/steps/community/detail-pages.steps.ts +3 -1
  5. package/e2e/src/steps/community/interactions.steps.ts +4 -4
  6. package/package.json +2 -2
  7. package/packages/builtin-agents/src/agents/group-supervisor/index.ts +1 -7
  8. package/packages/builtin-tool-group-agent-builder/src/ExecutionRuntime/index.ts +29 -0
  9. package/packages/builtin-tool-group-agent-builder/src/executor.ts +18 -0
  10. package/packages/builtin-tool-group-agent-builder/src/manifest.ts +17 -0
  11. package/packages/builtin-tool-group-agent-builder/src/types.ts +10 -0
  12. package/packages/builtin-tool-group-management/src/executor.test.ts +0 -12
  13. package/packages/builtin-tool-group-management/src/executor.ts +8 -47
  14. package/packages/builtin-tool-group-management/src/manifest.ts +0 -17
  15. package/packages/builtin-tool-group-management/src/systemRole.ts +1 -8
  16. package/packages/builtin-tool-group-management/src/types.ts +0 -10
  17. package/packages/builtin-tool-local-system/src/ExecutionRuntime/index.ts +70 -31
  18. package/packages/builtin-tool-local-system/src/executor/index.ts +94 -60
  19. package/packages/context-engine/src/processors/GroupMessageFlatten.ts +9 -6
  20. package/packages/context-engine/src/processors/__tests__/GroupMessageFlatten.test.ts +103 -0
  21. package/packages/context-engine/src/providers/GroupAgentBuilderContextInjector.ts +18 -31
  22. package/packages/context-engine/src/providers/__tests__/GroupAgentBuilderContextInjector.test.ts +307 -0
  23. package/packages/database/src/repositories/agentGroup/index.ts +23 -0
  24. package/packages/prompts/src/prompts/fileSystem/formatCommandOutput.test.ts +61 -0
  25. package/packages/prompts/src/prompts/fileSystem/formatCommandOutput.ts +21 -0
  26. package/packages/prompts/src/prompts/fileSystem/formatCommandResult.test.ts +87 -0
  27. package/packages/prompts/src/prompts/fileSystem/formatCommandResult.ts +35 -0
  28. package/packages/prompts/src/prompts/fileSystem/formatEditResult.test.ts +57 -0
  29. package/packages/prompts/src/prompts/fileSystem/formatEditResult.ts +17 -0
  30. package/packages/prompts/src/prompts/fileSystem/formatFileContent.test.ts +59 -0
  31. package/packages/prompts/src/prompts/fileSystem/formatFileContent.ts +14 -0
  32. package/packages/prompts/src/prompts/fileSystem/formatFileList.test.ts +62 -0
  33. package/packages/prompts/src/prompts/fileSystem/formatFileList.ts +13 -0
  34. package/packages/prompts/src/prompts/fileSystem/formatFileSearchResults.test.ts +34 -0
  35. package/packages/prompts/src/prompts/fileSystem/formatFileSearchResults.ts +12 -0
  36. package/packages/prompts/src/prompts/fileSystem/formatGlobResults.test.ts +64 -0
  37. package/packages/prompts/src/prompts/fileSystem/formatGlobResults.ts +23 -0
  38. package/packages/prompts/src/prompts/fileSystem/formatGrepResults.test.ts +85 -0
  39. package/packages/prompts/src/prompts/fileSystem/formatGrepResults.ts +24 -0
  40. package/packages/prompts/src/prompts/fileSystem/formatKillResult.test.ts +30 -0
  41. package/packages/prompts/src/prompts/fileSystem/formatKillResult.ts +9 -0
  42. package/packages/prompts/src/prompts/fileSystem/formatMoveResults.test.ts +37 -0
  43. package/packages/prompts/src/prompts/fileSystem/formatMoveResults.ts +20 -0
  44. package/packages/prompts/src/prompts/fileSystem/formatMultipleFiles.test.ts +54 -0
  45. package/packages/prompts/src/prompts/fileSystem/formatMultipleFiles.ts +9 -0
  46. package/packages/prompts/src/prompts/fileSystem/formatRenameResult.test.ts +35 -0
  47. package/packages/prompts/src/prompts/fileSystem/formatRenameResult.ts +17 -0
  48. package/packages/prompts/src/prompts/fileSystem/formatWriteResult.test.ts +30 -0
  49. package/packages/prompts/src/prompts/fileSystem/formatWriteResult.ts +11 -0
  50. package/packages/prompts/src/prompts/fileSystem/index.ts +13 -0
  51. package/packages/prompts/src/prompts/index.ts +1 -0
  52. package/packages/prompts/src/prompts/userMemory/__snapshots__/index.test.ts.snap +14 -38
  53. package/packages/prompts/src/prompts/userMemory/index.ts +5 -24
  54. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/Actions.tsx +4 -3
  55. package/src/app/[variants]/(main)/agent/_layout/Sidebar/Topic/useDropdownMenu.tsx +12 -2
  56. package/src/app/[variants]/(main)/community/(detail)/assistant/index.tsx +1 -1
  57. package/src/app/[variants]/(main)/community/(detail)/group_agent/features/Sidebar/ActionButton/AddGroupAgent.tsx +69 -17
  58. package/src/app/[variants]/(main)/community/(detail)/mcp/index.tsx +1 -1
  59. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/Actions.tsx +4 -3
  60. package/src/app/[variants]/(main)/group/_layout/Sidebar/Topic/useDropdownMenu.tsx +12 -2
  61. package/src/app/[variants]/(main)/group/features/Conversation/MainChatInput/index.tsx +2 -2
  62. package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentItem/index.tsx +2 -2
  63. package/src/features/ChatInput/ActionBar/Upload/ServerMode.tsx +13 -3
  64. package/src/features/ChatInput/ActionBar/components/ActionDropdown.tsx +26 -3
  65. package/src/features/Conversation/Messages/Supervisor/index.tsx +2 -1
  66. package/src/features/Conversation/Messages/components/ContentLoading.tsx +8 -2
  67. package/src/features/ResourceManager/components/Header/AddButton.tsx +20 -3
  68. package/src/server/routers/lambda/__tests__/agentGroup.test.ts +1 -0
  69. package/src/server/routers/lambda/agentGroup.ts +22 -0
  70. package/src/services/chat/index.ts +1 -0
  71. package/src/services/chat/mecha/agentConfigResolver.test.ts +62 -45
  72. package/src/services/chat/mecha/agentConfigResolver.ts +77 -10
  73. package/src/services/chat/mecha/modelParamsResolver.test.ts +211 -0
  74. package/src/services/chatGroup/index.ts +14 -0
  75. package/src/store/agentGroup/action.ts +30 -0
  76. package/src/store/agentGroup/slices/lifecycle.test.ts +77 -18
  77. package/src/store/agentGroup/slices/lifecycle.ts +7 -9
  78. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +6 -2
  79. package/src/store/chat/slices/operation/__tests__/selectors.test.ts +124 -0
  80. package/src/store/chat/slices/operation/selectors.ts +22 -0
@@ -8,6 +8,7 @@ import {
8
8
  type DropdownMenuProps,
9
9
  DropdownMenuRoot,
10
10
  DropdownMenuTrigger,
11
+ type MenuItemType,
11
12
  type MenuProps,
12
13
  type PopoverTrigger,
13
14
  renderDropdownMenuItems,
@@ -16,6 +17,7 @@ import { createStaticStyles, cx } from 'antd-style';
16
17
  import {
17
18
  type CSSProperties,
18
19
  type ReactNode,
20
+ isValidElement,
19
21
  memo,
20
22
  useCallback,
21
23
  useEffect,
@@ -34,8 +36,15 @@ const styles = createStaticStyles(({ css }) => ({
34
36
  `,
35
37
  }));
36
38
 
37
- type ActionDropdownMenu = Omit<Pick<MenuProps, 'className' | 'onClick' | 'style'>, 'items'> & {
38
- items: MenuProps['items'] | (() => MenuProps['items']);
39
+ export type ActionDropdownMenuItem = MenuItemType;
40
+
41
+ export type ActionDropdownMenuItems = MenuProps<ActionDropdownMenuItem>['items'];
42
+
43
+ type ActionDropdownMenu = Omit<
44
+ Pick<MenuProps<ActionDropdownMenuItem>, 'className' | 'onClick' | 'style'>,
45
+ 'items'
46
+ > & {
47
+ items: ActionDropdownMenuItems | (() => ActionDropdownMenuItems);
39
48
  };
40
49
 
41
50
  export interface ActionDropdownProps extends Omit<DropdownMenuProps, 'items'> {
@@ -116,7 +125,7 @@ const ActionDropdown = memo<ActionDropdownProps>(
116
125
  }, [openOnHover, triggerProps]);
117
126
 
118
127
  const decorateMenuItems = useCallback(
119
- (items: MenuProps['items']): MenuProps['items'] => {
128
+ (items: ActionDropdownMenuItems): ActionDropdownMenuItems => {
120
129
  if (!items) return items;
121
130
 
122
131
  return items.map((item) => {
@@ -136,10 +145,24 @@ const ActionDropdown = memo<ActionDropdownProps>(
136
145
  };
137
146
  }
138
147
  const itemOnClick = 'onClick' in item ? item.onClick : undefined;
148
+ const closeOnClick = 'closeOnClick' in item ? item.closeOnClick : undefined;
149
+ const keepOpenOnClick = closeOnClick === false;
150
+ const itemLabel = 'label' in item ? item.label : undefined;
151
+ const shouldKeepOpen = isValidElement(itemLabel);
152
+
153
+ const resolvedCloseOnClick = closeOnClick ?? (shouldKeepOpen ? false : undefined);
139
154
 
140
155
  return {
141
156
  ...item,
157
+ ...(resolvedCloseOnClick !== undefined ? { closeOnClick: resolvedCloseOnClick } : null),
142
158
  onClick: (info) => {
159
+ if (keepOpenOnClick) {
160
+ info.domEvent.stopPropagation();
161
+ menu.onClick?.(info);
162
+ itemOnClick?.(info);
163
+ return;
164
+ }
165
+
143
166
  info.domEvent.preventDefault();
144
167
  menu.onClick?.(info);
145
168
  itemOnClick?.(info);
@@ -55,7 +55,8 @@ const GroupMessage = memo<GroupMessageProps>(({ id, index, disableEditing, isLat
55
55
 
56
56
  // Get editing state from ConversationStore
57
57
  const creating = useConversationStore(messageStateSelectors.isMessageCreating(id));
58
- const newScreen = useNewScreen({ creating, isLatestItem });
58
+ const generating = useConversationStore(messageStateSelectors.isMessageGenerating(id));
59
+ const newScreen = useNewScreen({ creating: creating || generating, isLatestItem });
59
60
 
60
61
  const setMessageItemActionElementPortialContext = useSetMessageItemActionElementPortialContext();
61
62
  const setMessageItemActionTypeContext = useSetMessageItemActionTypeContext();
@@ -18,10 +18,11 @@ interface ContentLoadingProps {
18
18
  const ContentLoading = memo<ContentLoadingProps>(({ id }) => {
19
19
  const { t } = useTranslation('chat');
20
20
  const runningOp = useChatStore(operationSelectors.getDeepestRunningOperationByMessage(id));
21
+ console.log('runningOp', runningOp);
21
22
  const [elapsedSeconds, setElapsedSeconds] = useState(0);
23
+ const [startTime, setStartTime] = useState(runningOp?.metadata?.startTime);
22
24
 
23
25
  const operationType = runningOp?.type as OperationType | undefined;
24
- const startTime = runningOp?.metadata?.startTime;
25
26
 
26
27
  // Track elapsed time, reset when operation type changes
27
28
  useEffect(() => {
@@ -39,7 +40,12 @@ const ContentLoading = memo<ContentLoadingProps>(({ id }) => {
39
40
  const interval = setInterval(updateElapsed, 1000);
40
41
 
41
42
  return () => clearInterval(interval);
42
- }, [startTime, operationType]);
43
+ }, [startTime]);
44
+
45
+ useEffect(() => {
46
+ setElapsedSeconds(0);
47
+ setStartTime(Date.now());
48
+ }, [operationType, id]);
43
49
 
44
50
  // Get localized label based on operation type
45
51
  const operationLabel = operationType
@@ -5,7 +5,7 @@ import { Notion } from '@lobehub/icons';
5
5
  import { Button, DropdownMenu, Icon, type MenuProps } from '@lobehub/ui';
6
6
  import { Upload } from 'antd';
7
7
  import { FilePenLine, FileUp, FolderIcon, FolderUp, Link, Plus } from 'lucide-react';
8
- import { useCallback, useMemo } from 'react';
8
+ import { type ChangeEvent, useCallback, useMemo, useState } from 'react';
9
9
  import { useTranslation } from 'react-i18next';
10
10
 
11
11
  import { useResourceManagerStore } from '@/app/[variants]/(main)/resource/features/store';
@@ -23,6 +23,7 @@ const AddButton = () => {
23
23
  const pushDockFileList = useFileStore((s) => s.pushDockFileList);
24
24
  const uploadFolderWithStructure = useFileStore((s) => s.uploadFolderWithStructure);
25
25
  const createResourceAndSync = useFileStore((s) => s.createResourceAndSync);
26
+ const [menuOpen, setMenuOpen] = useState(false);
26
27
 
27
28
  // TODO: Migrate Notion import to use createResource
28
29
  // Keep old functions temporarily for components not yet migrated
@@ -121,6 +122,13 @@ const AddButton = () => {
121
122
  t,
122
123
  uploadFolderWithStructure,
123
124
  });
125
+ const handleFolderUploadWithClose = useCallback(
126
+ (event: ChangeEvent<HTMLInputElement>) => {
127
+ setMenuOpen(false);
128
+ return handleFolderUpload(event);
129
+ },
130
+ [handleFolderUpload],
131
+ );
124
132
 
125
133
  const items = useMemo<MenuProps['items']>(
126
134
  () => [
@@ -144,11 +152,13 @@ const AddButton = () => {
144
152
  type: 'divider',
145
153
  },
146
154
  {
155
+ closeOnClick: false,
147
156
  icon: <Icon icon={FileUp} />,
148
157
  key: 'upload-file',
149
158
  label: (
150
159
  <Upload
151
160
  beforeUpload={async (file) => {
161
+ setMenuOpen(false);
152
162
  await pushDockFileList([file], libraryId, currentFolderId ?? undefined);
153
163
 
154
164
  return false;
@@ -161,6 +171,7 @@ const AddButton = () => {
161
171
  ),
162
172
  },
163
173
  {
174
+ closeOnClick: false,
164
175
  icon: <Icon icon={FolderUp} />,
165
176
  key: 'upload-folder',
166
177
  label: <label htmlFor="folder-upload-input">{t('header.actions.uploadFolder')}</label>,
@@ -211,7 +222,13 @@ const AddButton = () => {
211
222
 
212
223
  return (
213
224
  <>
214
- <DropdownMenu items={items} placement="bottomRight" trigger="both">
225
+ <DropdownMenu
226
+ items={items}
227
+ onOpenChange={setMenuOpen}
228
+ open={menuOpen}
229
+ placement="bottomRight"
230
+ trigger="both"
231
+ >
215
232
  <Button data-no-highlight icon={Plus} type="primary">
216
233
  {t('addLibrary')}
217
234
  </Button>
@@ -233,7 +250,7 @@ const AddButton = () => {
233
250
  <input
234
251
  id="folder-upload-input"
235
252
  multiple
236
- onChange={handleFolderUpload}
253
+ onChange={handleFolderUploadWithClose}
237
254
  style={{ display: 'none' }}
238
255
  type="file"
239
256
  // @ts-expect-error - webkitdirectory is not in the React types
@@ -171,6 +171,7 @@ describe('agentGroupRouter', () => {
171
171
  config: { ...DEFAULT_CHAT_GROUP_CHAT_CONFIG, allowDM: true },
172
172
  },
173
173
  ['agent-1', 'agent-2'],
174
+ undefined,
174
175
  );
175
176
  expect(result).toEqual({
176
177
  agentIds: ['agent-1', 'agent-2'],
@@ -129,6 +129,19 @@ export const agentGroupRouter = router({
129
129
  })
130
130
  .partial(),
131
131
  ),
132
+ supervisorConfig: z
133
+ .object({
134
+ avatar: z.string().nullish(),
135
+ backgroundColor: z.string().nullish(),
136
+ description: z.string().nullish(),
137
+ model: z.string().nullish(),
138
+ params: z.any().nullish(),
139
+ provider: z.string().nullish(),
140
+ systemRole: z.string().nullish(),
141
+ tags: z.array(z.string()).nullish(),
142
+ title: z.string().nullish(),
143
+ })
144
+ .optional(),
132
145
  }),
133
146
  )
134
147
  .mutation(async ({ input, ctx }) => {
@@ -144,6 +157,14 @@ export const agentGroupRouter = router({
144
157
  const memberAgentIds = createdAgents.map((agent) => agent.id);
145
158
 
146
159
  // 2. Create group with supervisor and member agents
160
+ // Filter out null/undefined values from supervisorConfig
161
+ const supervisorConfig = input.supervisorConfig
162
+ ? Object.fromEntries(
163
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, eqeqeq
164
+ Object.entries(input.supervisorConfig).filter(([_, v]) => v != null),
165
+ )
166
+ : undefined;
167
+
147
168
  const { group, supervisorAgentId } = await ctx.agentGroupRepo.createGroupWithSupervisor(
148
169
  {
149
170
  ...input.groupConfig,
@@ -152,6 +173,7 @@ export const agentGroupRouter = router({
152
173
  ),
153
174
  },
154
175
  memberAgentIds,
176
+ supervisorConfig as any,
155
177
  );
156
178
 
157
179
  return { agentIds: memberAgentIds, groupId: group.id, supervisorAgentId };
@@ -138,6 +138,7 @@ class ChatService {
138
138
  plugins: pluginIds,
139
139
  } = resolveAgentConfig({
140
140
  agentId: targetAgentId,
141
+ groupId, // Pass groupId for supervisor detection
141
142
  model: payload.model,
142
143
  plugins: enabledPlugins,
143
144
  provider: payload.provider,
@@ -627,7 +627,7 @@ describe('resolveAgentConfig', () => {
627
627
  });
628
628
  });
629
629
 
630
- describe('supervisor agent (slug from agentGroup store)', () => {
630
+ describe('supervisor agent (detected via groupId)', () => {
631
631
  const mockGroupStoreState = { groupMap: {} };
632
632
  const mockGroupWithSupervisor = {
633
633
  agents: [
@@ -651,18 +651,11 @@ describe('resolveAgentConfig', () => {
651
651
  );
652
652
  });
653
653
 
654
- it('should detect supervisor agent from agentGroup store when not found in agent store', () => {
655
- // Mock: supervisor agent is found in agentGroup store
656
- vi.spyOn(
657
- agentGroupSelectors.agentGroupByIdSelectors,
658
- 'groupBySupervisorAgentId',
659
- ).mockReturnValue(() => mockGroupWithSupervisor as any);
660
-
661
- // Mock: getGroupBySupervisorAgentId for building groupSupervisorContext
662
- vi.spyOn(
663
- agentGroupSelectors.agentGroupSelectors,
664
- 'getGroupBySupervisorAgentId',
665
- ).mockReturnValue(() => mockGroupWithSupervisor as any);
654
+ it('should detect supervisor agent using groupId for direct lookup', () => {
655
+ // Mock: groupById returns the group
656
+ vi.spyOn(agentGroupSelectors.agentGroupByIdSelectors, 'groupById').mockReturnValue(
657
+ () => mockGroupWithSupervisor as any,
658
+ );
666
659
 
667
660
  // Mock: getGroupMembers returns non-supervisor agents
668
661
  vi.spyOn(agentGroupSelectors.agentGroupSelectors, 'getGroupMembers').mockReturnValue(
@@ -680,7 +673,10 @@ describe('resolveAgentConfig', () => {
680
673
  systemRole: 'You are a group supervisor...',
681
674
  });
682
675
 
683
- const result = resolveAgentConfig({ agentId: 'supervisor-agent-id' });
676
+ const result = resolveAgentConfig({
677
+ agentId: 'supervisor-agent-id',
678
+ groupId: 'group-123',
679
+ });
684
680
 
685
681
  expect(result.isBuiltinAgent).toBe(true);
686
682
  expect(result.slug).toBe('group-supervisor');
@@ -689,17 +685,10 @@ describe('resolveAgentConfig', () => {
689
685
  });
690
686
 
691
687
  it('should pass groupSupervisorContext to getAgentRuntimeConfig', () => {
692
- // Mock: supervisor agent is found in agentGroup store
693
- vi.spyOn(
694
- agentGroupSelectors.agentGroupByIdSelectors,
695
- 'groupBySupervisorAgentId',
696
- ).mockReturnValue(() => mockGroupWithSupervisor as any);
697
-
698
- // Mock: getGroupBySupervisorAgentId for building groupSupervisorContext
699
- vi.spyOn(
700
- agentGroupSelectors.agentGroupSelectors,
701
- 'getGroupBySupervisorAgentId',
702
- ).mockReturnValue(() => mockGroupWithSupervisor as any);
688
+ // Mock: groupById returns the group
689
+ vi.spyOn(agentGroupSelectors.agentGroupByIdSelectors, 'groupById').mockReturnValue(
690
+ () => mockGroupWithSupervisor as any,
691
+ );
703
692
 
704
693
  // Mock: getGroupMembers returns non-supervisor agents
705
694
  vi.spyOn(agentGroupSelectors.agentGroupSelectors, 'getGroupMembers').mockReturnValue(
@@ -718,7 +707,10 @@ describe('resolveAgentConfig', () => {
718
707
  systemRole: 'You are a group supervisor...',
719
708
  });
720
709
 
721
- resolveAgentConfig({ agentId: 'supervisor-agent-id' });
710
+ resolveAgentConfig({
711
+ agentId: 'supervisor-agent-id',
712
+ groupId: 'group-123',
713
+ });
722
714
 
723
715
  expect(getAgentRuntimeConfigSpy).toHaveBeenCalledWith(
724
716
  'group-supervisor',
@@ -736,31 +728,53 @@ describe('resolveAgentConfig', () => {
736
728
  );
737
729
  });
738
730
 
739
- it('should treat as regular agent when supervisor is not found in agentGroup store', () => {
740
- // Mock: supervisor agent is NOT found in agentGroup store
741
- vi.spyOn(
742
- agentGroupSelectors.agentGroupByIdSelectors,
743
- 'groupBySupervisorAgentId',
744
- ).mockReturnValue(() => undefined);
745
-
746
- const result = resolveAgentConfig({ agentId: 'some-other-agent' });
731
+ it('should treat as regular agent when groupId is not provided', () => {
732
+ // Without groupId, cannot detect supervisor
733
+ const result = resolveAgentConfig({ agentId: 'supervisor-agent-id' });
747
734
 
748
735
  expect(result.isBuiltinAgent).toBe(false);
749
736
  expect(result.slug).toBeUndefined();
750
737
  expect(result.plugins).toEqual(['plugin-a', 'plugin-b']); // Falls back to agent config plugins
751
738
  });
752
739
 
753
- it('should work correctly when regenerating supervisor message', () => {
754
- // This simulates the regenerate flow where agentId is the supervisor agent ID
755
- vi.spyOn(
756
- agentGroupSelectors.agentGroupByIdSelectors,
757
- 'groupBySupervisorAgentId',
758
- ).mockReturnValue(() => mockGroupWithSupervisor as any);
740
+ it('should treat as regular agent when agentId does not match group supervisorAgentId', () => {
741
+ // Mock: groupById returns the group
742
+ vi.spyOn(agentGroupSelectors.agentGroupByIdSelectors, 'groupById').mockReturnValue(
743
+ () => mockGroupWithSupervisor as any,
744
+ );
759
745
 
760
- vi.spyOn(
761
- agentGroupSelectors.agentGroupSelectors,
762
- 'getGroupBySupervisorAgentId',
763
- ).mockReturnValue(() => mockGroupWithSupervisor as any);
746
+ // Pass a different agentId that is not the supervisor
747
+ const result = resolveAgentConfig({
748
+ agentId: 'some-other-agent',
749
+ groupId: 'group-123',
750
+ });
751
+
752
+ expect(result.isBuiltinAgent).toBe(false);
753
+ expect(result.slug).toBeUndefined();
754
+ expect(result.plugins).toEqual(['plugin-a', 'plugin-b']);
755
+ });
756
+
757
+ it('should treat as regular agent when group is not found', () => {
758
+ // Mock: groupById returns undefined
759
+ vi.spyOn(agentGroupSelectors.agentGroupByIdSelectors, 'groupById').mockReturnValue(
760
+ () => undefined,
761
+ );
762
+
763
+ const result = resolveAgentConfig({
764
+ agentId: 'supervisor-agent-id',
765
+ groupId: 'non-existent-group',
766
+ });
767
+
768
+ expect(result.isBuiltinAgent).toBe(false);
769
+ expect(result.slug).toBeUndefined();
770
+ expect(result.plugins).toEqual(['plugin-a', 'plugin-b']);
771
+ });
772
+
773
+ it('should work correctly when regenerating supervisor message with groupId', () => {
774
+ // This simulates the regenerate flow where both agentId and groupId are provided
775
+ vi.spyOn(agentGroupSelectors.agentGroupByIdSelectors, 'groupById').mockReturnValue(
776
+ () => mockGroupWithSupervisor as any,
777
+ );
764
778
 
765
779
  vi.spyOn(agentGroupSelectors.agentGroupSelectors, 'getGroupMembers').mockReturnValue(
766
780
  () => [{ id: 'member-agent-1', title: 'Agent 1' }] as any,
@@ -772,7 +786,10 @@ describe('resolveAgentConfig', () => {
772
786
  systemRole: 'Supervisor system role',
773
787
  });
774
788
 
775
- const result = resolveAgentConfig({ agentId: 'supervisor-agent-id' });
789
+ const result = resolveAgentConfig({
790
+ agentId: 'supervisor-agent-id',
791
+ groupId: 'group-123',
792
+ });
776
793
 
777
794
  // Should correctly identify as builtin supervisor agent
778
795
  expect(result.isBuiltinAgent).toBe(true);
@@ -5,6 +5,7 @@ import {
5
5
  type LobeAgentConfig,
6
6
  type MessageMapScope,
7
7
  } from '@lobechat/types';
8
+ import debug from 'debug';
8
9
  import { produce } from 'immer';
9
10
 
10
11
  import { getAgentStoreState } from '@/store/agent';
@@ -12,6 +13,8 @@ import { agentSelectors, chatConfigByIdSelectors } from '@/store/agent/selectors
12
13
  import { getChatGroupStoreState } from '@/store/agentGroup';
13
14
  import { agentGroupByIdSelectors, agentGroupSelectors } from '@/store/agentGroup/selectors';
14
15
 
16
+ const log = debug('mecha:agentConfigResolver');
17
+
15
18
  /**
16
19
  * Applies params adjustments based on chatConfig settings.
17
20
  *
@@ -52,6 +55,12 @@ export interface AgentConfigResolverContext {
52
55
  /** Document content for page-agent */
53
56
  documentContent?: string;
54
57
 
58
+ /**
59
+ * Group ID for supervisor detection.
60
+ * When provided, used for direct lookup instead of iterating all groups.
61
+ */
62
+ groupId?: string;
63
+
55
64
  /** Current model being used (for template variables) */
56
65
  model?: string;
57
66
  /** Plugins enabled for the agent */
@@ -99,6 +108,8 @@ export interface ResolvedAgentConfig {
99
108
  export const resolveAgentConfig = (ctx: AgentConfigResolverContext): ResolvedAgentConfig => {
100
109
  const { agentId, model, documentContent, plugins, targetAgentConfig } = ctx;
101
110
 
111
+ log('resolveAgentConfig called with agentId: %s, scope: %s', agentId, ctx.scope);
112
+
102
113
  const agentStoreState = getAgentStoreState();
103
114
 
104
115
  // Get base config from store
@@ -109,21 +120,42 @@ export const resolveAgentConfig = (ctx: AgentConfigResolverContext): ResolvedAge
109
120
  const basePlugins = agentConfig.plugins ?? [];
110
121
 
111
122
  // Check if this is a builtin agent
112
- // First check agent store, then check if this is a supervisor agent in agentGroup store
123
+ // First check agent store, then check if this is a supervisor agent via groupId
113
124
  let slug = agentSelectors.getAgentSlugById(agentId)(agentStoreState);
125
+ log('slug from agentStore: %s (agentId: %s)', slug, agentId);
114
126
 
115
- // If not found in agent store, check if this is a supervisor agent in any group
116
- // Supervisor agents have their slug stored in agentGroup store, not agent store
117
- if (!slug) {
127
+ // If not found in agent store, check if this is a supervisor agent using groupId
128
+ // This is more reliable than iterating all groups to find a match
129
+ if (!slug && ctx.groupId) {
118
130
  const groupStoreState = getChatGroupStoreState();
119
- const group = agentGroupByIdSelectors.groupBySupervisorAgentId(agentId)(groupStoreState);
120
- if (group) {
121
- // This is a supervisor agent - use the builtin slug
131
+ const group = agentGroupByIdSelectors.groupById(ctx.groupId)(groupStoreState);
132
+
133
+ log(
134
+ 'checking supervisor via groupId %s: group=%o',
135
+ ctx.groupId,
136
+ group
137
+ ? {
138
+ groupId: group.id,
139
+ supervisorAgentId: group.supervisorAgentId,
140
+ title: group.title,
141
+ }
142
+ : null,
143
+ );
144
+
145
+ // Check if this agent is the supervisor of the specified group
146
+ if (group?.supervisorAgentId === agentId) {
122
147
  slug = BUILTIN_AGENT_SLUGS.groupSupervisor;
148
+ log(
149
+ 'agentId %s identified as group supervisor for group %s, assigned slug: %s',
150
+ agentId,
151
+ ctx.groupId,
152
+ slug,
153
+ );
123
154
  }
124
155
  }
125
156
 
126
157
  if (!slug) {
158
+ log('agentId %s is not a builtin agent (no slug found)', agentId);
127
159
  // Regular agent - use provided plugins if available, fallback to agent's plugins
128
160
  const finalPlugins = plugins && plugins.length > 0 ? plugins : basePlugins;
129
161
 
@@ -181,20 +213,49 @@ export const resolveAgentConfig = (ctx: AgentConfigResolverContext): ResolvedAge
181
213
  }
182
214
 
183
215
  // Build groupSupervisorContext if this is a group-supervisor agent
216
+ // Use groupId for direct lookup instead of reverse lookup by supervisorAgentId
184
217
  let groupSupervisorContext;
185
- if (slug === BUILTIN_AGENT_SLUGS.groupSupervisor) {
218
+ if (slug === BUILTIN_AGENT_SLUGS.groupSupervisor && ctx.groupId) {
219
+ log('building groupSupervisorContext for agentId: %s, groupId: %s', agentId, ctx.groupId);
186
220
  const groupStoreState = getChatGroupStoreState();
187
- // Find the group by supervisor agent ID
188
- const group = agentGroupSelectors.getGroupBySupervisorAgentId(agentId)(groupStoreState);
221
+ // Direct lookup using groupId
222
+ const group = agentGroupByIdSelectors.groupById(ctx.groupId)(groupStoreState);
223
+
224
+ log(
225
+ 'groupById result for %s: %o',
226
+ ctx.groupId,
227
+ group
228
+ ? {
229
+ agentsCount: group.agents?.length,
230
+ groupId: group.id,
231
+ supervisorAgentId: group.supervisorAgentId,
232
+ title: group.title,
233
+ }
234
+ : null,
235
+ );
189
236
 
190
237
  if (group) {
191
238
  const groupMembers = agentGroupSelectors.getGroupMembers(group.id)(groupStoreState);
239
+ log(
240
+ 'groupMembers for groupId %s: %o',
241
+ group.id,
242
+ groupMembers.map((m) => ({ id: m.id, isSupervisor: m.isSupervisor, title: m.title })),
243
+ );
244
+
192
245
  groupSupervisorContext = {
193
246
  availableAgents: groupMembers.map((agent) => ({ id: agent.id, title: agent.title })),
194
247
  groupId: group.id,
195
248
  groupTitle: group.title || 'Group Chat',
196
249
  systemPrompt: agentConfig.systemRole,
197
250
  };
251
+ log('groupSupervisorContext built: %o', {
252
+ availableAgentsCount: groupSupervisorContext.availableAgents.length,
253
+ groupId: groupSupervisorContext.groupId,
254
+ groupTitle: groupSupervisorContext.groupTitle,
255
+ hasSystemPrompt: !!groupSupervisorContext.systemPrompt,
256
+ });
257
+ } else {
258
+ log('WARNING: group not found for groupId: %s', ctx.groupId);
198
259
  }
199
260
  }
200
261
 
@@ -258,6 +319,12 @@ export const resolveAgentConfig = (ctx: AgentConfigResolverContext): ResolvedAge
258
319
  // Apply params adjustments based on chatConfig
259
320
  const finalAgentConfig = applyParamsFromChatConfig(resolvedAgentConfig, resolvedChatConfig);
260
321
 
322
+ log('resolveAgentConfig completed for agentId: %s, result: %o', agentId, {
323
+ isBuiltinAgent: true,
324
+ pluginsCount: finalPlugins.length,
325
+ slug,
326
+ });
327
+
261
328
  return {
262
329
  agentConfig: finalAgentConfig,
263
330
  chatConfig: resolvedChatConfig,