@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
@@ -800,4 +800,215 @@ describe('resolveModelExtendParams', () => {
800
800
  expect(modelExtendParamsSpy).toHaveBeenCalledWith('test-model', 'test-provider');
801
801
  });
802
802
  });
803
+
804
+ describe('parameter precedence and conflicts', () => {
805
+ beforeEach(() => {
806
+ vi.spyOn(aiModelSelectors.aiModelSelectors, 'isModelHasExtendParams').mockReturnValue(
807
+ () => true,
808
+ );
809
+ });
810
+
811
+ describe('reasoning effort variants precedence', () => {
812
+ it('should give precedence to later reasoning effort variants when multiple are configured', () => {
813
+ vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
814
+ 'reasoningEffort',
815
+ 'gpt5ReasoningEffort',
816
+ 'gpt5_1ReasoningEffort',
817
+ ]);
818
+
819
+ const result = resolveModelExtendParams({
820
+ chatConfig: {
821
+ gpt5_1ReasoningEffort: 'high',
822
+ gpt5ReasoningEffort: 'medium',
823
+ reasoningEffort: 'low',
824
+ } as any,
825
+ model: 'gpt-5.1',
826
+ provider: 'openai',
827
+ });
828
+
829
+ // gpt5_1ReasoningEffort should win as it's processed last
830
+ expect(result.reasoning_effort).toBe('high');
831
+ });
832
+
833
+ it('should handle mixed reasoning effort variants with only some configured', () => {
834
+ vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
835
+ 'reasoningEffort',
836
+ 'gpt5ReasoningEffort',
837
+ 'gpt5_2ReasoningEffort',
838
+ 'gpt5_2ProReasoningEffort',
839
+ ]);
840
+
841
+ const result = resolveModelExtendParams({
842
+ chatConfig: {
843
+ gpt5_2ProReasoningEffort: undefined,
844
+ gpt5_2ReasoningEffort: 'medium',
845
+ gpt5ReasoningEffort: undefined,
846
+ reasoningEffort: 'low',
847
+ } as any,
848
+ model: 'gpt-5.2',
849
+ provider: 'openai',
850
+ });
851
+
852
+ // gpt5_2ReasoningEffort should be set, others are undefined
853
+ expect(result.reasoning_effort).toBe('medium');
854
+ });
855
+
856
+ it('should use the last supported variant in processing order', () => {
857
+ vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
858
+ 'reasoningEffort',
859
+ 'gpt5_2ProReasoningEffort',
860
+ ]);
861
+
862
+ const result = resolveModelExtendParams({
863
+ chatConfig: {
864
+ gpt5_2ProReasoningEffort: 'high',
865
+ reasoningEffort: 'low',
866
+ } as any,
867
+ model: 'gpt-5.2-pro',
868
+ provider: 'openai',
869
+ });
870
+
871
+ // gpt5_2ProReasoningEffort is processed after reasoningEffort
872
+ expect(result.reasoning_effort).toBe('high');
873
+ });
874
+ });
875
+
876
+ describe('thinking configuration conflicts', () => {
877
+ it('should allow thinking type param to overwrite enableReasoning thinking config', () => {
878
+ vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
879
+ 'enableReasoning',
880
+ 'thinking',
881
+ ]);
882
+
883
+ const result = resolveModelExtendParams({
884
+ chatConfig: {
885
+ enableReasoning: true,
886
+ reasoningBudgetToken: 2048,
887
+ thinking: 'extended',
888
+ } as any,
889
+ model: 'model',
890
+ provider: 'provider',
891
+ });
892
+
893
+ // thinking param overwrites enableReasoning's thinking config
894
+ expect(result.thinking).toEqual({
895
+ type: 'extended',
896
+ });
897
+ });
898
+
899
+ it('should handle reasoningBudgetToken with thinking type param', () => {
900
+ vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
901
+ 'reasoningBudgetToken',
902
+ 'thinking',
903
+ ]);
904
+
905
+ const result = resolveModelExtendParams({
906
+ chatConfig: {
907
+ reasoningBudgetToken: 4096,
908
+ thinking: 'basic',
909
+ } as any,
910
+ model: 'model',
911
+ provider: 'provider',
912
+ });
913
+
914
+ // thinking param should overwrite the entire thinking config
915
+ expect(result.thinking).toEqual({
916
+ type: 'basic',
917
+ });
918
+ });
919
+
920
+ it('should combine independent thinking params without conflict', () => {
921
+ vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
922
+ 'thinking',
923
+ 'thinkingBudget',
924
+ 'thinkingLevel',
925
+ ]);
926
+
927
+ const result = resolveModelExtendParams({
928
+ chatConfig: {
929
+ thinking: 'enabled',
930
+ thinkingBudget: 5000,
931
+ thinkingLevel: 'advanced',
932
+ } as any,
933
+ model: 'model',
934
+ provider: 'provider',
935
+ });
936
+
937
+ // These are independent params and should all be set
938
+ expect(result.thinking).toEqual({ type: 'enabled' });
939
+ expect(result.thinkingBudget).toBe(5000);
940
+ expect(result.thinkingLevel).toBe('advanced');
941
+ });
942
+ });
943
+
944
+ describe('complex multi-parameter scenarios', () => {
945
+ it('should handle all reasoning variants with context caching and verbosity', () => {
946
+ vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
947
+ 'enableReasoning',
948
+ 'reasoningEffort',
949
+ 'gpt5ReasoningEffort',
950
+ 'disableContextCaching',
951
+ 'textVerbosity',
952
+ ]);
953
+
954
+ const result = resolveModelExtendParams({
955
+ chatConfig: {
956
+ disableContextCaching: true,
957
+ enableReasoning: true,
958
+ gpt5ReasoningEffort: 'high',
959
+ reasoningBudgetToken: 3000,
960
+ reasoningEffort: 'medium',
961
+ textVerbosity: 'verbose',
962
+ } as any,
963
+ model: 'gpt-5',
964
+ provider: 'openai',
965
+ });
966
+
967
+ expect(result).toEqual({
968
+ enabledContextCaching: false,
969
+ reasoning_effort: 'high',
970
+ thinking: {
971
+ budget_tokens: 3000,
972
+ type: 'enabled',
973
+ },
974
+ verbosity: 'verbose',
975
+ });
976
+ });
977
+
978
+ it('should handle all params when none are configured', () => {
979
+ vi.spyOn(aiModelSelectors.aiModelSelectors, 'modelExtendParams').mockReturnValue(() => [
980
+ 'enableReasoning',
981
+ 'reasoningEffort',
982
+ 'textVerbosity',
983
+ 'thinking',
984
+ 'thinkingBudget',
985
+ 'thinkingLevel',
986
+ 'urlContext',
987
+ 'imageAspectRatio',
988
+ 'imageResolution',
989
+ 'disableContextCaching',
990
+ ]);
991
+
992
+ const result = resolveModelExtendParams({
993
+ chatConfig: {} as any,
994
+ model: 'model',
995
+ provider: 'provider',
996
+ });
997
+
998
+ // Only enableReasoning should set thinking to disabled, others should be undefined
999
+ expect(result.thinking).toEqual({
1000
+ budget_tokens: 0,
1001
+ type: 'disabled',
1002
+ });
1003
+ expect(result.reasoning_effort).toBeUndefined();
1004
+ expect(result.verbosity).toBeUndefined();
1005
+ expect(result.thinkingBudget).toBeUndefined();
1006
+ expect(result.thinkingLevel).toBeUndefined();
1007
+ expect(result.urlContext).toBeUndefined();
1008
+ expect(result.imageAspectRatio).toBeUndefined();
1009
+ expect(result.imageResolution).toBeUndefined();
1010
+ expect(result.enabledContextCaching).toBeUndefined();
1011
+ });
1012
+ });
1013
+ });
803
1014
  });
@@ -20,6 +20,18 @@ export interface GroupMemberConfig {
20
20
  title?: string;
21
21
  }
22
22
 
23
+ export interface SupervisorConfig {
24
+ avatar?: string;
25
+ backgroundColor?: string;
26
+ description?: string;
27
+ model?: string;
28
+ params?: any;
29
+ provider?: string;
30
+ systemRole?: string;
31
+ tags?: string[];
32
+ title?: string;
33
+ }
34
+
23
35
  class ChatGroupService {
24
36
  /**
25
37
  * Create a group with a supervisor agent.
@@ -42,6 +54,7 @@ class ChatGroupService {
42
54
  createGroupWithMembers = (
43
55
  groupConfig: Omit<NewChatGroup, 'userId'>,
44
56
  members: GroupMemberConfig[],
57
+ supervisorConfig?: SupervisorConfig,
45
58
  ): Promise<{ agentIds: string[]; groupId: string; supervisorAgentId: string }> => {
46
59
  return lambdaClient.group.createGroupWithMembers.mutate({
47
60
  groupConfig: {
@@ -49,6 +62,7 @@ class ChatGroupService {
49
62
  config: groupConfig.config as any,
50
63
  },
51
64
  members: members as Partial<AgentItem>[],
65
+ supervisorConfig,
52
66
  });
53
67
  };
54
68
 
@@ -48,6 +48,12 @@ export interface ChatGroupInternalAction {
48
48
  type: string;
49
49
  },
50
50
  ) => void;
51
+ /**
52
+ * Fetch group detail directly and update store.
53
+ * Unlike refreshGroupDetail which uses SWR mutate, this method fetches immediately
54
+ * and is useful when SWR hook is not yet mounted (e.g., after createGroup).
55
+ */
56
+ internal_fetchGroupDetail: (groupId: string) => Promise<void>;
51
57
  internal_updateGroupMaps: (groups: ChatGroupItem[]) => void;
52
58
  loadGroups: () => Promise<void>;
53
59
  refreshGroupDetail: (groupId: string) => Promise<void>;
@@ -91,6 +97,30 @@ const chatGroupInternalSlice: StateCreator<
91
97
  return {
92
98
  internal_dispatchChatGroup: dispatch,
93
99
 
100
+ internal_fetchGroupDetail: async (groupId: string) => {
101
+ const groupDetail = await chatGroupService.getGroupDetail(groupId);
102
+ if (!groupDetail) return;
103
+
104
+ // Update groupMap with full group detail including supervisorAgentId and agents
105
+ dispatch({ payload: { id: groupDetail.id, value: groupDetail }, type: 'updateGroup' });
106
+
107
+ // Sync group agents to agentStore for builtin agent resolution
108
+ const agentStore = getAgentStoreState();
109
+ for (const agent of groupDetail.agents) {
110
+ agentStore.internal_dispatchAgentMap(agent.id, agent as any);
111
+ }
112
+
113
+ // Set activeAgentId to supervisor for correct model resolution
114
+ if (groupDetail.supervisorAgentId) {
115
+ agentStore.setActiveAgentId(groupDetail.supervisorAgentId);
116
+ useChatStore.setState(
117
+ { activeAgentId: groupDetail.supervisorAgentId },
118
+ false,
119
+ 'syncActiveAgentIdFromAgentGroup',
120
+ );
121
+ }
122
+ },
123
+
94
124
  internal_updateGroupMaps: (groups) => {
95
125
  // Build a candidate map from incoming groups
96
126
  const incomingMap = groups.reduce(
@@ -10,19 +10,31 @@ vi.mock('@/services/chatGroup', () => ({
10
10
  chatGroupService: {
11
11
  addAgentsToGroup: vi.fn(),
12
12
  createGroup: vi.fn(),
13
+ getGroupDetail: vi.fn(),
13
14
  getGroups: vi.fn(),
14
15
  },
15
16
  }));
16
17
 
17
- vi.mock('@/store/session', () => ({
18
- getSessionStoreState: vi.fn(() => ({
19
- activeId: 'some-session-id',
20
- refreshSessions: vi.fn().mockResolvedValue(undefined),
21
- sessions: [],
22
- switchSession: vi.fn(),
18
+ vi.mock('@/store/home', () => ({
19
+ getHomeStoreState: vi.fn(() => ({
20
+ refreshAgentList: vi.fn(),
21
+ switchToGroup: vi.fn(),
23
22
  })),
24
23
  }));
25
24
 
25
+ vi.mock('@/store/agent', () => ({
26
+ getAgentStoreState: vi.fn(() => ({
27
+ internal_dispatchAgentMap: vi.fn(),
28
+ setActiveAgentId: vi.fn(),
29
+ })),
30
+ }));
31
+
32
+ vi.mock('@/store/chat', () => ({
33
+ useChatStore: {
34
+ setState: vi.fn(),
35
+ },
36
+ }));
37
+
26
38
  describe('ChatGroupLifecycleSlice', () => {
27
39
  beforeEach(() => {
28
40
  vi.clearAllMocks();
@@ -47,12 +59,17 @@ describe('ChatGroupLifecycleSlice', () => {
47
59
  title: 'Test Group',
48
60
  userId: 'user-1',
49
61
  };
62
+ const mockGroupDetail = {
63
+ ...mockGroup,
64
+ agents: [],
65
+ supervisorAgentId: 'supervisor-1',
66
+ };
50
67
 
51
68
  vi.mocked(chatGroupService.createGroup).mockResolvedValue({
52
69
  group: mockGroup as any,
53
70
  supervisorAgentId: 'supervisor-1',
54
71
  });
55
- vi.mocked(chatGroupService.getGroups).mockResolvedValue([mockGroup as any]);
72
+ vi.mocked(chatGroupService.getGroupDetail).mockResolvedValue(mockGroupDetail as any);
56
73
 
57
74
  const { result } = renderHook(() => useAgentGroupStore());
58
75
 
@@ -71,13 +88,18 @@ describe('ChatGroupLifecycleSlice', () => {
71
88
  title: 'Test Group',
72
89
  userId: 'user-1',
73
90
  };
91
+ const mockGroupDetail = {
92
+ ...mockGroup,
93
+ agents: [],
94
+ supervisorAgentId: 'supervisor-1',
95
+ };
74
96
 
75
97
  vi.mocked(chatGroupService.createGroup).mockResolvedValue({
76
98
  group: mockGroup as any,
77
99
  supervisorAgentId: 'supervisor-1',
78
100
  });
79
101
  vi.mocked(chatGroupService.addAgentsToGroup).mockResolvedValue({ added: [], existing: [] });
80
- vi.mocked(chatGroupService.getGroups).mockResolvedValue([mockGroup as any]);
102
+ vi.mocked(chatGroupService.getGroupDetail).mockResolvedValue(mockGroupDetail as any);
81
103
 
82
104
  const { result } = renderHook(() => useAgentGroupStore());
83
105
 
@@ -91,14 +113,46 @@ describe('ChatGroupLifecycleSlice', () => {
91
113
  ]);
92
114
  });
93
115
 
94
- it('should not switch session when silent is true', async () => {
95
- const mockSwitchSession = vi.fn();
96
- const { getSessionStoreState } = await import('@/store/session');
97
- vi.mocked(getSessionStoreState).mockReturnValue({
98
- activeId: 'some-session-id',
99
- refreshSessions: vi.fn().mockResolvedValue(undefined),
100
- sessions: [],
101
- switchSession: mockSwitchSession,
116
+ it('should fetch group detail and store supervisorAgentId for tools injection', async () => {
117
+ const mockGroup = {
118
+ id: 'new-group-id',
119
+ title: 'Test Group',
120
+ userId: 'user-1',
121
+ };
122
+ const mockSupervisorAgentId = 'supervisor-agent-123';
123
+ const mockGroupDetail = {
124
+ ...mockGroup,
125
+ agents: [],
126
+ supervisorAgentId: mockSupervisorAgentId,
127
+ };
128
+
129
+ vi.mocked(chatGroupService.createGroup).mockResolvedValue({
130
+ group: mockGroup as any,
131
+ supervisorAgentId: mockSupervisorAgentId,
132
+ });
133
+ vi.mocked(chatGroupService.getGroupDetail).mockResolvedValue(mockGroupDetail as any);
134
+
135
+ const { result } = renderHook(() => useAgentGroupStore());
136
+
137
+ await act(async () => {
138
+ await result.current.createGroup({ title: 'Test Group' });
139
+ });
140
+
141
+ // Verify getGroupDetail was called to fetch full group info
142
+ expect(chatGroupService.getGroupDetail).toHaveBeenCalledWith('new-group-id');
143
+
144
+ // Verify supervisorAgentId is stored in groupMap for tools injection
145
+ const groupDetail = result.current.groupMap['new-group-id'];
146
+ expect(groupDetail).toBeDefined();
147
+ expect(groupDetail.supervisorAgentId).toBe(mockSupervisorAgentId);
148
+ });
149
+
150
+ it('should not switch to group when silent is true', async () => {
151
+ const mockSwitchToGroup = vi.fn();
152
+ const { getHomeStoreState } = await import('@/store/home');
153
+ vi.mocked(getHomeStoreState).mockReturnValue({
154
+ refreshAgentList: vi.fn(),
155
+ switchToGroup: mockSwitchToGroup,
102
156
  } as any);
103
157
 
104
158
  const mockGroup = {
@@ -106,12 +160,17 @@ describe('ChatGroupLifecycleSlice', () => {
106
160
  title: 'Test Group',
107
161
  userId: 'user-1',
108
162
  };
163
+ const mockGroupDetail = {
164
+ ...mockGroup,
165
+ agents: [],
166
+ supervisorAgentId: 'supervisor-1',
167
+ };
109
168
 
110
169
  vi.mocked(chatGroupService.createGroup).mockResolvedValue({
111
170
  group: mockGroup as any,
112
171
  supervisorAgentId: 'supervisor-1',
113
172
  });
114
- vi.mocked(chatGroupService.getGroups).mockResolvedValue([mockGroup as any]);
173
+ vi.mocked(chatGroupService.getGroupDetail).mockResolvedValue(mockGroupDetail as any);
115
174
 
116
175
  const { result } = renderHook(() => useAgentGroupStore());
117
176
 
@@ -119,7 +178,7 @@ describe('ChatGroupLifecycleSlice', () => {
119
178
  await result.current.createGroup({ title: 'Test Group' }, [], true);
120
179
  });
121
180
 
122
- expect(mockSwitchSession).not.toHaveBeenCalled();
181
+ expect(mockSwitchToGroup).not.toHaveBeenCalled();
123
182
  });
124
183
  });
125
184
  });
@@ -5,7 +5,7 @@ import { type StateCreator } from 'zustand/vanilla';
5
5
  import { chatGroupService } from '@/services/chatGroup';
6
6
  import { type ChatGroupStore } from '@/store/agentGroup/store';
7
7
  import { useChatStore } from '@/store/chat';
8
- import { getSessionStoreState } from '@/store/session';
8
+ import { getHomeStoreState } from '@/store/home';
9
9
 
10
10
  export interface ChatGroupLifecycleAction {
11
11
  createGroup: (
@@ -14,7 +14,6 @@ export interface ChatGroupLifecycleAction {
14
14
  silent?: boolean,
15
15
  ) => Promise<string>;
16
16
  /**
17
- * @deprecated Use switchTopic(undefined) instead
18
17
  * Switch to a new topic in the group
19
18
  * Clears activeTopicId and navigates to group root
20
19
  */
@@ -32,11 +31,8 @@ export const chatGroupLifecycleSlice: StateCreator<
32
31
  [],
33
32
  ChatGroupLifecycleAction
34
33
  > = (_, get) => ({
35
- /**
36
- * @param silent - if true, do not switch to the new group session
37
- */
38
34
  createGroup: async (newGroup, agentIds, silent = false) => {
39
- const { switchSession } = getSessionStoreState();
35
+ const { switchToGroup, refreshAgentList } = getHomeStoreState();
40
36
 
41
37
  const { group } = await chatGroupService.createGroup(newGroup);
42
38
 
@@ -52,11 +48,13 @@ export const chatGroupLifecycleSlice: StateCreator<
52
48
 
53
49
  get().internal_dispatchChatGroup({ payload: group, type: 'addGroup' });
54
50
 
55
- await get().loadGroups();
56
- await getSessionStoreState().refreshSessions();
51
+ // Fetch full group detail to get supervisorAgentId and agents for tools injection
52
+ await get().internal_fetchGroupDetail(group.id);
53
+
54
+ refreshAgentList();
57
55
 
58
56
  if (!silent) {
59
- switchSession(group.id);
57
+ switchToGroup(group.id);
60
58
  }
61
59
 
62
60
  return group.id;
@@ -160,14 +160,16 @@ export const streamingExecutor: StateCreator<
160
160
  // - agentId is used for session ID (message storage location)
161
161
  const effectiveAgentId = paramSubAgentId || agentId;
162
162
 
163
- // Get scope from operation context if available
163
+ // Get scope and groupId from operation context if available
164
164
  const operation = operationId ? get().operations[operationId] : undefined;
165
165
  const scope = operation?.context.scope;
166
+ const groupId = operation?.context.groupId;
166
167
 
167
168
  // Resolve agent config with builtin agent runtime config merged
168
169
  // This ensures runtime plugins (e.g., 'lobe-agent-builder' for Agent Builder) are included
169
170
  const { agentConfig: agentConfigData, plugins: pluginIds } = resolveAgentConfig({
170
171
  agentId: effectiveAgentId || '',
172
+ groupId, // Pass groupId for supervisor detection
171
173
  scope, // Pass scope from operation context
172
174
  });
173
175
 
@@ -341,6 +343,7 @@ export const streamingExecutor: StateCreator<
341
343
  // - max_tokens/reasoning_effort based on chatConfig settings
342
344
  const resolved = resolveAgentConfig({
343
345
  agentId: effectiveAgentId,
346
+ groupId, // Pass groupId for supervisor detection
344
347
  scope, // scope is already available from line 329
345
348
  });
346
349
  const finalAgentConfig = agentConfig || resolved.agentConfig;
@@ -594,7 +597,8 @@ export const streamingExecutor: StateCreator<
594
597
  // - max_tokens/reasoning_effort based on chatConfig settings
595
598
  const { agentConfig: agentConfigData } = resolveAgentConfig({
596
599
  agentId: effectiveAgentId || '',
597
- scope: context.scope, // Pass scope from context parameter (available at line 883)
600
+ groupId, // Pass groupId for supervisor detection
601
+ scope: context.scope, // Pass scope from context parameter
598
602
  });
599
603
 
600
604
  // Use agent config from agentId
@@ -509,6 +509,130 @@ describe('Operation Selectors', () => {
509
509
  });
510
510
  });
511
511
 
512
+ describe('isAgentRunning', () => {
513
+ it('should return false when no operations exist', () => {
514
+ const { result } = renderHook(() => useChatStore());
515
+
516
+ expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(false);
517
+ });
518
+
519
+ it('should return true only for the agent with running operations', () => {
520
+ const { result } = renderHook(() => useChatStore());
521
+
522
+ act(() => {
523
+ result.current.startOperation({
524
+ type: 'execAgentRuntime',
525
+ context: { agentId: 'agent1' },
526
+ });
527
+ });
528
+
529
+ expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(true);
530
+ expect(operationSelectors.isAgentRunning('agent2')(result.current)).toBe(false);
531
+ });
532
+
533
+ it('should return false when operation completes', () => {
534
+ const { result } = renderHook(() => useChatStore());
535
+
536
+ let opId: string;
537
+
538
+ act(() => {
539
+ opId = result.current.startOperation({
540
+ type: 'execAgentRuntime',
541
+ context: { agentId: 'agent1' },
542
+ }).operationId;
543
+ });
544
+
545
+ expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(true);
546
+
547
+ act(() => {
548
+ result.current.completeOperation(opId!);
549
+ });
550
+
551
+ expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(false);
552
+ });
553
+
554
+ it('should exclude aborting operations', () => {
555
+ const { result } = renderHook(() => useChatStore());
556
+
557
+ let opId: string;
558
+
559
+ act(() => {
560
+ opId = result.current.startOperation({
561
+ type: 'execAgentRuntime',
562
+ context: { agentId: 'agent1' },
563
+ }).operationId;
564
+ });
565
+
566
+ expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(true);
567
+
568
+ act(() => {
569
+ result.current.updateOperationMetadata(opId!, { isAborting: true });
570
+ });
571
+
572
+ expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(false);
573
+ });
574
+
575
+ it('should detect any topic with running operations for the agent', () => {
576
+ const { result } = renderHook(() => useChatStore());
577
+
578
+ act(() => {
579
+ // Agent 1, topic 1
580
+ result.current.startOperation({
581
+ type: 'execAgentRuntime',
582
+ context: { agentId: 'agent1', topicId: 'topic1' },
583
+ });
584
+
585
+ // Agent 1, topic 2
586
+ result.current.startOperation({
587
+ type: 'execAgentRuntime',
588
+ context: { agentId: 'agent1', topicId: 'topic2' },
589
+ });
590
+
591
+ // Agent 2, topic 3
592
+ result.current.startOperation({
593
+ type: 'execAgentRuntime',
594
+ context: { agentId: 'agent2', topicId: 'topic3' },
595
+ });
596
+ });
597
+
598
+ // Agent 1 should be running (has 2 topics with operations)
599
+ expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(true);
600
+ // Agent 2 should also be running
601
+ expect(operationSelectors.isAgentRunning('agent2')(result.current)).toBe(true);
602
+ // Agent 3 should not be running
603
+ expect(operationSelectors.isAgentRunning('agent3')(result.current)).toBe(false);
604
+ });
605
+
606
+ it('should detect server agent runtime operations', () => {
607
+ const { result } = renderHook(() => useChatStore());
608
+
609
+ act(() => {
610
+ result.current.startOperation({
611
+ type: 'execServerAgentRuntime',
612
+ context: { agentId: 'agent1', groupId: 'group1' },
613
+ });
614
+ });
615
+
616
+ expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(true);
617
+ expect(operationSelectors.isAgentRunning('agent2')(result.current)).toBe(false);
618
+ });
619
+
620
+ it('should not detect non-AI-runtime operations', () => {
621
+ const { result } = renderHook(() => useChatStore());
622
+
623
+ act(() => {
624
+ // sendMessage is not an AI runtime operation type
625
+ result.current.startOperation({
626
+ type: 'sendMessage',
627
+ context: { agentId: 'agent1' },
628
+ });
629
+ });
630
+
631
+ // sendMessage is not in AI_RUNTIME_OPERATION_TYPES, so should return false
632
+ expect(operationSelectors.isAgentRunning('agent1')(result.current)).toBe(false);
633
+ });
634
+ });
635
+
512
636
  describe('backward compatibility selectors', () => {
513
637
  it('isAgentRuntimeRunning should work', () => {
514
638
  const { result } = renderHook(() => useChatStore());