@lobehub/lobehub 2.0.0-next.27 → 2.0.0-next.29

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 (34) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/package.json +1 -1
  4. package/packages/database/package.json +1 -1
  5. package/packages/database/src/models/message.ts +29 -2
  6. package/packages/types/src/discover/mcp.ts +6 -0
  7. package/packages/types/src/message/ui/params.ts +0 -49
  8. package/packages/types/src/plugins/mcp.ts +4 -1
  9. package/renovate.json +4 -30
  10. package/src/features/MCP/utils.test.ts +91 -0
  11. package/src/features/MCP/utils.ts +20 -2
  12. package/src/features/PluginStore/Content.tsx +2 -3
  13. package/src/features/PluginStore/McpList/index.tsx +6 -2
  14. package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +35 -35
  15. package/src/server/routers/lambda/market/index.ts +4 -2
  16. package/src/server/routers/lambda/message.ts +104 -16
  17. package/src/services/mcp.ts +40 -6
  18. package/src/services/message/index.ts +63 -35
  19. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +17 -10
  20. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +4 -4
  21. package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +2 -2
  22. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +3 -2
  23. package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +2 -2
  24. package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +7 -2
  25. package/src/store/chat/slices/builtinTool/actions/search.ts +3 -3
  26. package/src/store/chat/slices/message/action.test.ts +152 -15
  27. package/src/store/chat/slices/message/action.ts +70 -82
  28. package/src/store/chat/slices/plugin/action.test.ts +84 -25
  29. package/src/store/chat/slices/plugin/action.ts +52 -24
  30. package/src/store/chat/slices/thread/action.test.ts +13 -4
  31. package/src/store/chat/slices/thread/action.ts +3 -1
  32. package/src/store/tool/slices/mcpStore/action.test.ts +95 -3
  33. package/src/store/tool/slices/mcpStore/action.ts +177 -53
  34. package/src/store/tool/slices/oldStore/initialState.ts +1 -2
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  ChatMessageError,
4
4
  ChatMessagePluginError,
5
- ChatTranslate,
6
5
  ChatTTS,
6
+ ChatTranslate,
7
7
  CreateMessageParams,
8
8
  CreateMessageResult,
9
9
  ModelRankItem,
@@ -20,12 +20,9 @@ import { useUserStore } from '@/store/user';
20
20
  import { labPreferSelectors } from '@/store/user/selectors';
21
21
 
22
22
  export class MessageService {
23
- createMessage = async ({ sessionId, ...params }: CreateMessageParams): Promise<string> => {
24
- return lambdaClient.message.createMessage.mutate({
25
- ...params,
26
- sessionId: sessionId ? this.toDbSessionId(sessionId) : undefined,
27
- });
28
- };
23
+ private get useGroup() {
24
+ return labPreferSelectors.enableAssistantMessageGroup(useUserStore.getState());
25
+ }
29
26
 
30
27
  createNewMessage = async ({
31
28
  sessionId,
@@ -42,27 +39,21 @@ export class MessageService {
42
39
  topicId?: string,
43
40
  groupId?: string,
44
41
  ): Promise<UIChatMessage[]> => {
45
- // Get user lab preference for message grouping
46
- const useGroup = labPreferSelectors.enableAssistantMessageGroup(useUserStore.getState());
47
-
48
42
  const data = await lambdaClient.message.getMessages.query({
49
43
  groupId,
50
44
  sessionId: this.toDbSessionId(sessionId),
51
45
  topicId,
52
- useGroup,
46
+ useGroup: this.useGroup,
53
47
  });
54
48
 
55
49
  return data as unknown as UIChatMessage[];
56
50
  };
57
51
 
58
52
  getGroupMessages = async (groupId: string, topicId?: string): Promise<UIChatMessage[]> => {
59
- // Get user lab preference for message grouping
60
- const useGroup = labPreferSelectors.enableAssistantMessageGroup(useUserStore.getState());
61
-
62
53
  const data = await lambdaClient.message.getMessages.query({
63
54
  groupId,
64
55
  topicId,
65
- useGroup,
56
+ useGroup: this.useGroup,
66
57
  });
67
58
  return data as unknown as UIChatMessage[];
68
59
  };
@@ -109,6 +100,7 @@ export class MessageService {
109
100
  id,
110
101
  sessionId: options?.sessionId,
111
102
  topicId: options?.topicId,
103
+ useGroup: this.useGroup,
112
104
  value,
113
105
  });
114
106
  };
@@ -121,24 +113,70 @@ export class MessageService {
121
113
  return lambdaClient.message.updateTTS.mutate({ id, value: tts });
122
114
  };
123
115
 
124
- updateMessagePluginState = async (id: string, value: Record<string, any>) => {
125
- return lambdaClient.message.updatePluginState.mutate({ id, value });
116
+ updateMessagePluginState = async (
117
+ id: string,
118
+ value: Record<string, any>,
119
+ options?: { sessionId?: string | null; topicId?: string | null },
120
+ ): Promise<UpdateMessageResult> => {
121
+ return lambdaClient.message.updatePluginState.mutate({
122
+ id,
123
+ sessionId: options?.sessionId,
124
+ topicId: options?.topicId,
125
+ useGroup: this.useGroup,
126
+ value,
127
+ });
126
128
  };
127
129
 
128
- updateMessagePluginError = async (id: string, error: ChatMessagePluginError | null) => {
129
- return lambdaClient.message.updatePluginError.mutate({ id, value: error as any });
130
+ updateMessagePluginError = async (
131
+ id: string,
132
+ error: ChatMessagePluginError | null,
133
+ options?: { sessionId?: string | null; topicId?: string | null },
134
+ ): Promise<UpdateMessageResult> => {
135
+ return lambdaClient.message.updatePluginError.mutate({
136
+ id,
137
+ sessionId: options?.sessionId,
138
+ topicId: options?.topicId,
139
+ useGroup: this.useGroup,
140
+ value: error as any,
141
+ });
130
142
  };
131
143
 
132
- updateMessageRAG = async (id: string, data: UpdateMessageRAGParams): Promise<void> => {
133
- return lambdaClient.message.updateMessageRAG.mutate({ id, value: data });
144
+ updateMessageRAG = async (
145
+ id: string,
146
+ data: UpdateMessageRAGParams,
147
+ options?: { sessionId?: string | null; topicId?: string | null },
148
+ ): Promise<UpdateMessageResult> => {
149
+ return lambdaClient.message.updateMessageRAG.mutate({
150
+ id,
151
+ sessionId: options?.sessionId,
152
+ topicId: options?.topicId,
153
+ useGroup: this.useGroup,
154
+ value: data,
155
+ });
134
156
  };
135
157
 
136
- removeMessage = async (id: string) => {
137
- return lambdaClient.message.removeMessage.mutate({ id });
158
+ removeMessage = async (
159
+ id: string,
160
+ options?: { sessionId?: string | null; topicId?: string | null },
161
+ ): Promise<UpdateMessageResult> => {
162
+ return lambdaClient.message.removeMessage.mutate({
163
+ id,
164
+ sessionId: options?.sessionId,
165
+ topicId: options?.topicId,
166
+ useGroup: this.useGroup,
167
+ });
138
168
  };
139
169
 
140
- removeMessages = async (ids: string[]) => {
141
- return lambdaClient.message.removeMessages.mutate({ ids });
170
+ removeMessages = async (
171
+ ids: string[],
172
+ options?: { sessionId?: string | null; topicId?: string | null },
173
+ ): Promise<UpdateMessageResult> => {
174
+ return lambdaClient.message.removeMessages.mutate({
175
+ ids,
176
+ sessionId: options?.sessionId,
177
+ topicId: options?.topicId,
178
+ useGroup: this.useGroup,
179
+ });
142
180
  };
143
181
 
144
182
  removeMessagesByAssistant = async (sessionId: string, topicId?: string) => {
@@ -162,16 +200,6 @@ export class MessageService {
162
200
  private toDbSessionId = (sessionId: string | undefined) => {
163
201
  return sessionId === INBOX_SESSION_ID ? null : sessionId;
164
202
  };
165
-
166
- hasMessages = async (): Promise<boolean> => {
167
- const number = await this.countMessages();
168
- return number > 0;
169
- };
170
-
171
- messageCountToCheckTrace = async (): Promise<boolean> => {
172
- const number = await this.countMessages();
173
- return number >= 4;
174
- };
175
203
  }
176
204
 
177
205
  export const messageService = new MessageService();
@@ -61,7 +61,7 @@ describe('chatMessage actions', () => {
61
61
  await result.current.sendMessage({ message: TEST_CONTENT.USER_MESSAGE });
62
62
  });
63
63
 
64
- expect(messageService.createMessage).not.toHaveBeenCalled();
64
+ expect(messageService.createNewMessage).not.toHaveBeenCalled();
65
65
  expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
66
66
  });
67
67
 
@@ -72,7 +72,7 @@ describe('chatMessage actions', () => {
72
72
  await result.current.sendMessage({ message: TEST_CONTENT.EMPTY });
73
73
  });
74
74
 
75
- expect(messageService.createMessage).not.toHaveBeenCalled();
75
+ expect(messageService.createNewMessage).not.toHaveBeenCalled();
76
76
  });
77
77
 
78
78
  it('should not send when message is empty with empty files array', async () => {
@@ -82,7 +82,7 @@ describe('chatMessage actions', () => {
82
82
  await result.current.sendMessage({ message: TEST_CONTENT.EMPTY, files: [] });
83
83
  });
84
84
 
85
- expect(messageService.createMessage).not.toHaveBeenCalled();
85
+ expect(messageService.createNewMessage).not.toHaveBeenCalled();
86
86
  });
87
87
  });
88
88
 
@@ -97,13 +97,13 @@ describe('chatMessage actions', () => {
97
97
  });
98
98
  });
99
99
 
100
- expect(messageService.createMessage).toHaveBeenCalled();
100
+ expect(messageService.createNewMessage).toHaveBeenCalled();
101
101
  expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
102
102
  });
103
103
 
104
104
  it('should handle message creation errors gracefully', async () => {
105
105
  const { result } = renderHook(() => useChatStore());
106
- vi.spyOn(messageService, 'createMessage').mockRejectedValue(
106
+ vi.spyOn(messageService, 'createNewMessage').mockRejectedValue(
107
107
  new Error('create message error'),
108
108
  );
109
109
 
@@ -210,6 +210,8 @@ describe('chatMessage actions', () => {
210
210
 
211
211
  describe('internal_coreProcessMessage', () => {
212
212
  it('should process user message and generate AI response', async () => {
213
+ const mockMessages = [{ id: 'msg-1', content: 'test' }] as any;
214
+
213
215
  act(() => {
214
216
  useChatStore.setState({ internal_coreProcessMessage: realCoreProcessMessage });
215
217
  });
@@ -227,8 +229,10 @@ describe('chatMessage actions', () => {
227
229
  .mockResolvedValue({ isFunctionCall: false, content: 'AI response' });
228
230
 
229
231
  const createMessageSpy = vi
230
- .spyOn(messageService, 'createMessage')
231
- .mockResolvedValue(TEST_IDS.ASSISTANT_MESSAGE_ID);
232
+ .spyOn(messageService, 'createNewMessage')
233
+ .mockResolvedValue({ id: TEST_IDS.ASSISTANT_MESSAGE_ID, messages: mockMessages });
234
+
235
+ const replaceMessagesSpy = vi.spyOn(result.current, 'replaceMessages');
232
236
 
233
237
  await act(async () => {
234
238
  await result.current.internal_coreProcessMessage([userMessage], userMessage.id);
@@ -245,7 +249,7 @@ describe('chatMessage actions', () => {
245
249
  );
246
250
 
247
251
  expect(fetchAIChatSpy).toHaveBeenCalled();
248
- expect(result.current.refreshMessages).toHaveBeenCalled();
252
+ expect(replaceMessagesSpy).toHaveBeenCalledWith(mockMessages);
249
253
  });
250
254
 
251
255
  it('should handle RAG flow when ragQuery is provided', async () => {
@@ -268,7 +272,10 @@ describe('chatMessage actions', () => {
268
272
  rewriteQuery: 'rewritten query',
269
273
  });
270
274
 
271
- vi.spyOn(messageService, 'createMessage').mockResolvedValue(TEST_IDS.ASSISTANT_MESSAGE_ID);
275
+ vi.spyOn(messageService, 'createNewMessage').mockResolvedValue({
276
+ id: TEST_IDS.ASSISTANT_MESSAGE_ID,
277
+ messages: [],
278
+ });
272
279
 
273
280
  await act(async () => {
274
281
  await result.current.internal_coreProcessMessage([userMessage], userMessage.id, {
@@ -299,7 +306,7 @@ describe('chatMessage actions', () => {
299
306
  .spyOn(result.current, 'internal_fetchAIChatMessage')
300
307
  .mockResolvedValue({ isFunctionCall: false, content: '' });
301
308
 
302
- vi.spyOn(messageService, 'createMessage').mockResolvedValue(undefined as any);
309
+ vi.spyOn(messageService, 'createNewMessage').mockResolvedValue(undefined as any);
303
310
 
304
311
  await act(async () => {
305
312
  await result.current.internal_coreProcessMessage([userMessage], userMessage.id);
@@ -131,7 +131,7 @@ describe('generateAIChatV2 actions', () => {
131
131
  await result.current.sendMessage({ message: TEST_CONTENT.USER_MESSAGE });
132
132
  });
133
133
 
134
- expect(messageService.createMessage).not.toHaveBeenCalled();
134
+ expect(messageService.createNewMessage).not.toHaveBeenCalled();
135
135
  expect(result.current.internal_execAgentRuntime).not.toHaveBeenCalled();
136
136
  });
137
137
 
@@ -142,7 +142,7 @@ describe('generateAIChatV2 actions', () => {
142
142
  await result.current.sendMessage({ message: TEST_CONTENT.EMPTY });
143
143
  });
144
144
 
145
- expect(messageService.createMessage).not.toHaveBeenCalled();
145
+ expect(messageService.createNewMessage).not.toHaveBeenCalled();
146
146
  });
147
147
 
148
148
  it('should not send when message is empty with empty files array', async () => {
@@ -152,7 +152,7 @@ describe('generateAIChatV2 actions', () => {
152
152
  await result.current.sendMessage({ message: TEST_CONTENT.EMPTY, files: [] });
153
153
  });
154
154
 
155
- expect(messageService.createMessage).not.toHaveBeenCalled();
155
+ expect(messageService.createNewMessage).not.toHaveBeenCalled();
156
156
  });
157
157
  });
158
158
 
@@ -312,7 +312,7 @@ describe('generateAIChatV2 actions', () => {
312
312
  });
313
313
  });
314
314
 
315
- expect(messageService.createMessage).toHaveBeenCalled();
315
+ expect(messageService.createNewMessage).toHaveBeenCalled();
316
316
  expect(result.current.internal_execAgentRuntime).not.toHaveBeenCalled();
317
317
  });
318
318
 
@@ -58,8 +58,8 @@ export const createMockAbortController = () => {
58
58
  */
59
59
  export const spyOnMessageService = () => {
60
60
  const createMessageSpy = vi
61
- .spyOn(messageService, 'createMessage')
62
- .mockResolvedValue(TEST_IDS.NEW_MESSAGE_ID);
61
+ .spyOn(messageService, 'createNewMessage')
62
+ .mockResolvedValue({ id: TEST_IDS.NEW_MESSAGE_ID, messages: [] });
63
63
  const updateMessageSpy = vi
64
64
  .spyOn(messageService, 'updateMessage')
65
65
  .mockResolvedValue({ messages: [], success: true });
@@ -220,9 +220,10 @@ export const generateAIChat: StateCreator<
220
220
  ragQueryId,
221
221
  };
222
222
 
223
- const assistantId = await get().internal_createMessage(assistantMessage);
223
+ const result = await get().internal_createMessage(assistantMessage);
224
224
 
225
- if (!assistantId) return;
225
+ if (!result) return;
226
+ const assistantId = result.id;
226
227
 
227
228
  // 3. place a search with the search working model if this model is not support tool use
228
229
  const aiInfraStoreState = getAiInfraStoreState();
@@ -659,7 +659,7 @@ export const generateAIChatV2: StateCreator<
659
659
  groupId: groupMessage.groupId, // Propagate groupId from parent message for group chat
660
660
  };
661
661
 
662
- const result = await get().internal_createNewMessage(toolMessage);
662
+ const result = await get().internal_createMessage(toolMessage);
663
663
 
664
664
  if (!result) {
665
665
  log('[triggerToolsCalling] Failed to create tool message for %s', payload.identifier);
@@ -768,7 +768,7 @@ export const generateAIChatV2: StateCreator<
768
768
  topicId: get().activeTopicId,
769
769
  });
770
770
 
771
- const result = await get().internal_createNewMessage(assistantMessage, { groupMessageId });
771
+ const result = await get().internal_createMessage(assistantMessage, { groupMessageId });
772
772
 
773
773
  if (!result) {
774
774
  log('[callToolFollowAssistantMessage] Failed to create assistant message');
@@ -294,7 +294,7 @@ export const chatAiGroupChat: StateCreator<
294
294
  targetId: targetMemberId,
295
295
  };
296
296
 
297
- const messageId = await internal_createMessage(userMessage);
297
+ const result = await internal_createMessage(userMessage);
298
298
 
299
299
  // if only add user message, then stop
300
300
  if (onlyAddUserMessage) {
@@ -302,6 +302,9 @@ export const chatAiGroupChat: StateCreator<
302
302
  return;
303
303
  }
304
304
 
305
+ if (!result) return;
306
+ const messageId = result.id;
307
+
305
308
  if (messageId) {
306
309
  // Use the specific group's config rather than relying on active session
307
310
  const groupConfig = selectGroupConfig(groupId);
@@ -668,7 +671,9 @@ export const chatAiGroupChat: StateCreator<
668
671
 
669
672
  console.log('DEBUG: Creating agent message with:', agentMessage);
670
673
 
671
- const assistantId = await internal_createMessage(agentMessage);
674
+ const result = await internal_createMessage(agentMessage);
675
+ if (!result) return;
676
+ const assistantId = result.id;
672
677
 
673
678
  const systemMessage: UIChatMessage = {
674
679
  id: 'group-system',
@@ -106,16 +106,16 @@ export const searchSlice: StateCreator<
106
106
  });
107
107
  };
108
108
 
109
- const [newMessageId] = await Promise.all([
109
+ const [result] = await Promise.all([
110
110
  // 1. 添加 tool message
111
111
  internal_createMessage(toolMessage),
112
112
  // 2. 将这条 tool call message 插入到 ai 消息的 tools 中
113
113
  addToolItem(),
114
114
  ]);
115
- if (!newMessageId) return;
115
+ if (!result) return;
116
116
 
117
117
  // 将新创建的 tool message 激活
118
- openToolUI(newMessageId, message.plugin.identifier);
118
+ openToolUI(result.id, message.plugin.identifier);
119
119
  },
120
120
 
121
121
  search: async (id, params, aiSummary = true) => {
@@ -25,7 +25,7 @@ vi.mock('@/services/message', () => ({
25
25
  removeMessage: vi.fn(),
26
26
  removeMessagesByAssistant: vi.fn(),
27
27
  removeMessages: vi.fn(() => Promise.resolve()),
28
- createMessage: vi.fn(() => Promise.resolve('new-message-id')),
28
+ createNewMessage: vi.fn(() => Promise.resolve({ id: 'new-message-id', messages: [] })),
29
29
  updateMessage: vi.fn(),
30
30
  removeAllMessages: vi.fn(() => Promise.resolve()),
31
31
  },
@@ -71,7 +71,7 @@ describe('chatMessage actions', () => {
71
71
  await result.current.addAIMessage();
72
72
  });
73
73
 
74
- expect(messageService.createMessage).not.toHaveBeenCalled();
74
+ expect(messageService.createNewMessage).not.toHaveBeenCalled();
75
75
  expect(updateInputMessageSpy).not.toHaveBeenCalled();
76
76
  });
77
77
 
@@ -84,7 +84,7 @@ describe('chatMessage actions', () => {
84
84
  await result.current.addAIMessage();
85
85
  });
86
86
 
87
- expect(messageService.createMessage).toHaveBeenCalledWith({
87
+ expect(messageService.createNewMessage).toHaveBeenCalledWith({
88
88
  content: inputMessage,
89
89
  role: 'assistant',
90
90
  sessionId: mockState.activeId,
@@ -113,7 +113,7 @@ describe('chatMessage actions', () => {
113
113
  await result.current.addUserMessage({ message: 'test message' });
114
114
  });
115
115
 
116
- expect(messageService.createMessage).not.toHaveBeenCalled();
116
+ expect(messageService.createNewMessage).not.toHaveBeenCalled();
117
117
  expect(updateInputMessageSpy).not.toHaveBeenCalled();
118
118
  });
119
119
 
@@ -130,7 +130,7 @@ describe('chatMessage actions', () => {
130
130
  await result.current.addUserMessage({ message, fileList });
131
131
  });
132
132
 
133
- expect(messageService.createMessage).toHaveBeenCalledWith({
133
+ expect(messageService.createNewMessage).toHaveBeenCalledWith({
134
134
  content: message,
135
135
  files: fileList,
136
136
  role: 'user',
@@ -154,7 +154,7 @@ describe('chatMessage actions', () => {
154
154
  await result.current.addUserMessage({ message });
155
155
  });
156
156
 
157
- expect(messageService.createMessage).toHaveBeenCalledWith({
157
+ expect(messageService.createNewMessage).toHaveBeenCalledWith({
158
158
  content: message,
159
159
  files: undefined,
160
160
  role: 'user',
@@ -184,7 +184,7 @@ describe('chatMessage actions', () => {
184
184
  await result.current.addUserMessage({ message });
185
185
  });
186
186
 
187
- expect(messageService.createMessage).toHaveBeenCalledWith({
187
+ expect(messageService.createNewMessage).toHaveBeenCalledWith({
188
188
  content: message,
189
189
  files: undefined,
190
190
  role: 'user',
@@ -200,6 +200,15 @@ describe('chatMessage actions', () => {
200
200
  const { result } = renderHook(() => useChatStore());
201
201
  const messageId = 'message-id';
202
202
  const deleteSpy = vi.spyOn(result.current, 'deleteMessage');
203
+ const mockMessages = [{ id: 'other-message' }] as any;
204
+
205
+ // Mock the service to return messages
206
+ (messageService.removeMessages as Mock).mockResolvedValue({
207
+ success: true,
208
+ messages: mockMessages,
209
+ });
210
+
211
+ const replaceMessagesSpy = vi.spyOn(result.current, 'replaceMessages');
203
212
 
204
213
  act(() => {
205
214
  useChatStore.setState({
@@ -215,13 +224,22 @@ describe('chatMessage actions', () => {
215
224
  });
216
225
 
217
226
  expect(deleteSpy).toHaveBeenCalledWith(messageId);
218
- expect(result.current.refreshMessages).toHaveBeenCalled();
227
+ expect(replaceMessagesSpy).toHaveBeenCalledWith(mockMessages);
219
228
  });
220
229
 
221
230
  it('deleteMessage should remove messages with tools', async () => {
222
231
  const { result } = renderHook(() => useChatStore());
223
232
  const messageId = 'message-id';
224
233
  const removeMessagesSpy = vi.spyOn(messageService, 'removeMessages');
234
+ const mockMessages = [{ id: 'remaining-message' }] as any;
235
+
236
+ // Mock the service to return messages
237
+ (messageService.removeMessages as Mock).mockResolvedValue({
238
+ success: true,
239
+ messages: mockMessages,
240
+ });
241
+
242
+ const replaceMessagesSpy = vi.spyOn(result.current, 'replaceMessages');
225
243
 
226
244
  act(() => {
227
245
  useChatStore.setState({
@@ -240,8 +258,120 @@ describe('chatMessage actions', () => {
240
258
  await result.current.deleteMessage(messageId);
241
259
  });
242
260
 
243
- expect(removeMessagesSpy).toHaveBeenCalledWith([messageId, '2', '3']);
244
- expect(result.current.refreshMessages).toHaveBeenCalled();
261
+ expect(removeMessagesSpy).toHaveBeenCalledWith([messageId, '2', '3'], {
262
+ sessionId: 'session-id',
263
+ topicId: undefined,
264
+ });
265
+ expect(replaceMessagesSpy).toHaveBeenCalledWith(mockMessages);
266
+ });
267
+
268
+ it('deleteMessage should remove group message with all children', async () => {
269
+ const { result } = renderHook(() => useChatStore());
270
+ const groupMessageId = 'group-message-id';
271
+ const removeMessagesSpy = vi.spyOn(messageService, 'removeMessages');
272
+ const mockMessages = [{ id: 'remaining-message' }] as any;
273
+
274
+ // Mock the service to return messages
275
+ (messageService.removeMessages as Mock).mockResolvedValue({
276
+ success: true,
277
+ messages: mockMessages,
278
+ });
279
+
280
+ const replaceMessagesSpy = vi.spyOn(result.current, 'replaceMessages');
281
+
282
+ act(() => {
283
+ useChatStore.setState({
284
+ activeId: 'session-id',
285
+ activeTopicId: undefined,
286
+ messagesMap: {
287
+ [messageMapKey('session-id')]: [
288
+ { id: groupMessageId, role: 'group', content: 'Group message' } as UIChatMessage,
289
+ {
290
+ id: 'child-1',
291
+ parentId: groupMessageId,
292
+ role: 'assistant',
293
+ content: 'Child 1',
294
+ } as UIChatMessage,
295
+ {
296
+ id: 'child-2',
297
+ parentId: groupMessageId,
298
+ role: 'assistant',
299
+ content: 'Child 2',
300
+ } as UIChatMessage,
301
+ { id: 'other-message', role: 'user', content: 'Other' } as UIChatMessage,
302
+ ],
303
+ },
304
+ });
305
+ });
306
+ await act(async () => {
307
+ await result.current.deleteMessage(groupMessageId);
308
+ });
309
+
310
+ expect(removeMessagesSpy).toHaveBeenCalledWith([groupMessageId, 'child-1', 'child-2'], {
311
+ sessionId: 'session-id',
312
+ topicId: undefined,
313
+ });
314
+ expect(replaceMessagesSpy).toHaveBeenCalledWith(mockMessages);
315
+ });
316
+
317
+ it('deleteMessage should remove group message with children that have tool calls', async () => {
318
+ const { result } = renderHook(() => useChatStore());
319
+ const groupMessageId = 'group-message-id';
320
+ const removeMessagesSpy = vi.spyOn(messageService, 'removeMessages');
321
+ const mockMessages = [{ id: 'remaining-message' }] as any;
322
+
323
+ // Mock the service to return messages
324
+ (messageService.removeMessages as Mock).mockResolvedValue({
325
+ success: true,
326
+ messages: mockMessages,
327
+ });
328
+
329
+ const replaceMessagesSpy = vi.spyOn(result.current, 'replaceMessages');
330
+
331
+ act(() => {
332
+ useChatStore.setState({
333
+ activeId: 'session-id',
334
+ activeTopicId: undefined,
335
+ messagesMap: {
336
+ [messageMapKey('session-id')]: [
337
+ { id: groupMessageId, role: 'group', content: 'Group message' } as UIChatMessage,
338
+ {
339
+ id: 'child-1',
340
+ parentId: groupMessageId,
341
+ role: 'assistant',
342
+ content: 'Child with tools',
343
+ tools: [{ id: 'tool1' }],
344
+ } as UIChatMessage,
345
+ {
346
+ id: 'tool-result-1',
347
+ tool_call_id: 'tool1',
348
+ role: 'tool',
349
+ content: 'Tool result',
350
+ } as UIChatMessage,
351
+ {
352
+ id: 'child-2',
353
+ parentId: groupMessageId,
354
+ role: 'assistant',
355
+ content: 'Child 2',
356
+ } as UIChatMessage,
357
+ { id: 'other-message', role: 'user', content: 'Other' } as UIChatMessage,
358
+ ],
359
+ },
360
+ });
361
+ });
362
+ await act(async () => {
363
+ await result.current.deleteMessage(groupMessageId);
364
+ });
365
+
366
+ // Should delete group message + all children + tool results of children
367
+ expect(removeMessagesSpy).toHaveBeenCalledWith(
368
+ [groupMessageId, 'child-1', 'child-2', 'tool-result-1'],
369
+ {
370
+ sessionId: 'session-id',
371
+ topicId: undefined,
372
+ },
373
+ );
374
+ expect(replaceMessagesSpy).toHaveBeenCalledWith(mockMessages);
245
375
  });
246
376
  });
247
377
 
@@ -309,10 +439,16 @@ describe('chatMessage actions', () => {
309
439
  });
310
440
 
311
441
  expect(removeMessageSpy).toHaveBeenCalled();
312
- expect(updateMessageSpy).toHaveBeenCalledWith('message-id', {
313
- tools: [{ id: 'tool2' }],
314
- });
315
- expect(result.current.refreshMessages).toHaveBeenCalled();
442
+ expect(updateMessageSpy).toHaveBeenCalledWith(
443
+ 'message-id',
444
+ {
445
+ tools: [{ id: 'tool2' }],
446
+ },
447
+ {
448
+ sessionId: 'session-id',
449
+ topicId: undefined,
450
+ },
451
+ );
316
452
  });
317
453
  });
318
454
 
@@ -320,13 +456,14 @@ describe('chatMessage actions', () => {
320
456
  it('clearAllMessages should remove all messages', async () => {
321
457
  const { result } = renderHook(() => useChatStore());
322
458
  const clearAllSpy = vi.spyOn(result.current, 'clearAllMessages');
459
+ const replaceMessagesSpy = vi.spyOn(result.current, 'replaceMessages');
323
460
 
324
461
  await act(async () => {
325
462
  await result.current.clearAllMessages();
326
463
  });
327
464
 
328
465
  expect(clearAllSpy).toHaveBeenCalled();
329
- expect(result.current.refreshMessages).toHaveBeenCalled();
466
+ expect(replaceMessagesSpy).toHaveBeenCalledWith([]);
330
467
  });
331
468
  });
332
469