@lobehub/chat 1.2.6 → 1.2.7

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 (32) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/package.json +1 -1
  3. package/src/app/(main)/chat/(workspace)/@portal/features/ArtifactUI/Footer.tsx +1 -1
  4. package/src/app/(main)/chat/(workspace)/@portal/features/Header.tsx +1 -1
  5. package/src/app/(main)/chat/(workspace)/_layout/Mobile/PortalModal.tsx +35 -0
  6. package/src/app/(main)/chat/(workspace)/_layout/Mobile/index.tsx +3 -1
  7. package/src/database/client/models/__tests__/message.test.ts +13 -0
  8. package/src/database/client/models/message.ts +4 -0
  9. package/src/database/server/models/__tests__/message.test.ts +103 -0
  10. package/src/database/server/models/message.ts +13 -6
  11. package/src/features/Conversation/Actions/Tool.tsx +12 -6
  12. package/src/features/Conversation/Messages/Tool/Inspector/index.tsx +25 -19
  13. package/src/features/Conversation/Messages/Tool/Inspector/style.ts +9 -0
  14. package/src/server/routers/lambda/message.ts +7 -1
  15. package/src/services/message/client.test.ts +16 -2
  16. package/src/services/message/client.ts +5 -1
  17. package/src/services/message/server.ts +7 -2
  18. package/src/services/message/type.ts +2 -1
  19. package/src/store/chat/slices/message/action.test.ts +144 -0
  20. package/src/store/chat/slices/message/action.ts +111 -64
  21. package/src/store/chat/slices/message/reducer.test.ts +200 -1
  22. package/src/store/chat/slices/message/reducer.ts +62 -2
  23. package/src/store/chat/slices/plugin/action.test.ts +42 -0
  24. package/src/store/chat/slices/plugin/action.ts +46 -0
  25. package/src/store/chat/slices/portal/action.test.ts +6 -6
  26. package/src/store/chat/slices/portal/action.ts +3 -3
  27. package/src/store/chat/slices/topic/action.test.ts +3 -2
  28. package/src/store/chat/slices/topic/action.ts +1 -1
  29. package/src/store/global/action.test.ts +13 -0
  30. package/src/store/global/action.ts +7 -0
  31. package/src/store/global/initialState.ts +1 -0
  32. package/src/store/global/selectors.ts +2 -0
@@ -30,6 +30,7 @@ vi.mock('@/services/message', () => ({
30
30
  getMessages: vi.fn(),
31
31
  updateMessageError: vi.fn(),
32
32
  removeMessage: vi.fn(),
33
+ removeMessagesByAssistant: vi.fn(),
33
34
  removeMessages: vi.fn(() => Promise.resolve()),
34
35
  createMessage: vi.fn(() => Promise.resolve('new-message-id')),
35
36
  updateMessage: vi.fn(),
@@ -147,6 +148,124 @@ describe('chatMessage actions', () => {
147
148
  expect(deleteSpy).toHaveBeenCalledWith(messageId);
148
149
  expect(result.current.refreshMessages).toHaveBeenCalled();
149
150
  });
151
+
152
+ it('deleteMessage should remove messages with tools', async () => {
153
+ const { result } = renderHook(() => useChatStore());
154
+ const messageId = 'message-id';
155
+ const removeMessagesSpy = vi.spyOn(messageService, 'removeMessages');
156
+
157
+ act(() => {
158
+ useChatStore.setState({
159
+ activeId: 'session-id',
160
+ activeTopicId: undefined,
161
+ messagesMap: {
162
+ [messageMapKey('session-id')]: [
163
+ { id: messageId, tools: [{ id: 'tool1' }, { id: 'tool2' }] } as ChatMessage,
164
+ { id: '2', tool_call_id: 'tool1', role: 'tool' } as ChatMessage,
165
+ { id: '3', tool_call_id: 'tool2', role: 'tool' } as ChatMessage,
166
+ ],
167
+ },
168
+ });
169
+ });
170
+ await act(async () => {
171
+ await result.current.deleteMessage(messageId);
172
+ });
173
+
174
+ expect(removeMessagesSpy).toHaveBeenCalledWith([messageId, '2', '3']);
175
+ expect(result.current.refreshMessages).toHaveBeenCalled();
176
+ });
177
+ });
178
+
179
+ describe('deleteToolMessage', () => {
180
+ it('deleteMessage should remove a message by id', async () => {
181
+ const { result } = renderHook(() => useChatStore());
182
+ const messageId = 'message-id';
183
+ const updateMessageSpy = vi.spyOn(messageService, 'updateMessage');
184
+ const removeMessageSpy = vi.spyOn(messageService, 'removeMessage');
185
+
186
+ act(() => {
187
+ useChatStore.setState({
188
+ activeId: 'session-id',
189
+ activeTopicId: undefined,
190
+ messagesMap: {
191
+ [messageMapKey('session-id')]: [
192
+ {
193
+ id: messageId,
194
+ role: 'assistant',
195
+ tools: [{ id: 'tool1' }, { id: 'tool2' }],
196
+ } as ChatMessage,
197
+ { id: '2', parentId: messageId, tool_call_id: 'tool1', role: 'tool' } as ChatMessage,
198
+ { id: '3', tool_call_id: 'tool2', role: 'tool' } as ChatMessage,
199
+ ],
200
+ },
201
+ });
202
+ });
203
+ await act(async () => {
204
+ await result.current.deleteToolMessage('2');
205
+ });
206
+
207
+ expect(removeMessageSpy).toHaveBeenCalled();
208
+ expect(updateMessageSpy).toHaveBeenCalledWith('message-id', {
209
+ tools: [{ id: 'tool2' }],
210
+ });
211
+ expect(result.current.refreshMessages).toHaveBeenCalled();
212
+ });
213
+ });
214
+
215
+ describe('delAndRegenerateMessage', () => {
216
+ it('should remove a message and create a new message', async () => {
217
+ const { result } = renderHook(() => useChatStore());
218
+ const messageId = 'message-id';
219
+ const deleteMessageSpy = vi.spyOn(result.current, 'deleteMessage');
220
+ const resendMessageSpy = vi.spyOn(result.current, 'internal_resendMessage');
221
+
222
+ act(() => {
223
+ useChatStore.setState({
224
+ activeId: 'session-id',
225
+ activeTopicId: undefined,
226
+ messagesMap: {
227
+ [messageMapKey('session-id')]: [
228
+ { id: messageId, tools: [{ id: 'tool1' }, { id: 'tool2' }] } as ChatMessage,
229
+ ],
230
+ },
231
+ });
232
+ });
233
+ await act(async () => {
234
+ await result.current.delAndRegenerateMessage(messageId);
235
+ });
236
+
237
+ expect(deleteMessageSpy).toHaveBeenCalledWith(messageId);
238
+ expect(resendMessageSpy).toHaveBeenCalled();
239
+ expect(result.current.refreshMessages).toHaveBeenCalled();
240
+ });
241
+ });
242
+ describe('regenerateMessage', () => {
243
+ it('should create a new message', async () => {
244
+ const { result } = renderHook(() => useChatStore());
245
+ const messageId = 'message-id';
246
+ const resendMessageSpy = vi.spyOn(result.current, 'internal_resendMessage');
247
+
248
+ act(() => {
249
+ useChatStore.setState({
250
+ activeId: 'session-id',
251
+ activeTopicId: undefined,
252
+ messagesMap: {
253
+ [messageMapKey('session-id')]: [
254
+ {
255
+ id: messageId,
256
+ tools: [{ id: 'tool1' }, { id: 'tool2' }],
257
+ traceId: 'abc',
258
+ } as ChatMessage,
259
+ ],
260
+ },
261
+ });
262
+ });
263
+ await act(async () => {
264
+ await result.current.regenerateMessage(messageId);
265
+ });
266
+
267
+ expect(resendMessageSpy).toHaveBeenCalledWith(messageId, 'abc');
268
+ });
150
269
  });
151
270
 
152
271
  describe('clearAllMessages', () => {
@@ -1144,6 +1263,31 @@ describe('chatMessage actions', () => {
1144
1263
  });
1145
1264
  });
1146
1265
 
1266
+ describe('internal_toggleToolCallingStreaming action', () => {
1267
+ it('should add message id to messageLoadingIds when loading is true', () => {
1268
+ const { result } = renderHook(() => useChatStore());
1269
+ const messageId = 'message-id';
1270
+
1271
+ act(() => {
1272
+ result.current.internal_toggleToolCallingStreaming(messageId, [true]);
1273
+ });
1274
+
1275
+ expect(result.current.toolCallingStreamIds[messageId]).toEqual([true]);
1276
+ });
1277
+
1278
+ it('should remove message id from messageLoadingIds when loading is false', () => {
1279
+ const { result } = renderHook(() => useChatStore());
1280
+ const messageId = 'ddd-id';
1281
+
1282
+ act(() => {
1283
+ result.current.internal_toggleToolCallingStreaming(messageId, [true]);
1284
+ result.current.internal_toggleToolCallingStreaming(messageId, undefined);
1285
+ });
1286
+
1287
+ expect(result.current.toolCallingStreamIds[messageId]).toBeUndefined();
1288
+ });
1289
+ });
1290
+
1147
1291
  describe('stopGenerateMessage', () => {
1148
1292
  it('should return early if abortController is undefined', () => {
1149
1293
  act(() => {
@@ -66,6 +66,7 @@ export interface ChatMessageAction {
66
66
  */
67
67
  clearMessage: () => Promise<void>;
68
68
  deleteMessage: (id: string) => Promise<void>;
69
+ deleteToolMessage: (id: string) => Promise<void>;
69
70
  delAndRegenerateMessage: (id: string) => Promise<void>;
70
71
  clearAllMessages: () => Promise<void>;
71
72
  // update
@@ -81,19 +82,7 @@ export interface ChatMessageAction {
81
82
  // ========= ↓ Internal Method ↓ ========== //
82
83
  // ========================================== //
83
84
  // ========================================== //
84
- internal_toggleChatLoading: (
85
- loading: boolean,
86
- id?: string,
87
- action?: string,
88
- ) => AbortController | undefined;
89
- internal_toggleLoadingArrays: (
90
- key: keyof ChatStoreState,
91
- loading: boolean,
92
- id?: string,
93
- action?: string,
94
- ) => AbortController | undefined;
95
- internal_toggleToolCallingStreaming: (id: string, streaming: boolean[] | undefined) => void;
96
- internal_toggleMessageLoading: (loading: boolean, id: string) => void;
85
+
97
86
  /**
98
87
  * update message at the frontend point
99
88
  * this method will not update messages to database
@@ -108,9 +97,7 @@ export interface ChatMessageAction {
108
97
  params?: ProcessMessageParams,
109
98
  ) => Promise<void>;
110
99
  /**
111
- * 实际获取 AI 响应
112
- * @param messages - 聊天消息数组
113
- * @param options - 获取 SSE 选项
100
+ * the method to fetch the AI message
114
101
  */
115
102
  internal_fetchAIChatMessage: (
116
103
  messages: ChatMessage[],
@@ -122,24 +109,66 @@ export interface ChatMessageAction {
122
109
  }>;
123
110
 
124
111
  /**
112
+ * update the message content with optimistic update
125
113
  * a method used by other action
126
- * @param id
127
- * @param content
128
114
  */
129
115
  internal_updateMessageContent: (
130
116
  id: string,
131
117
  content: string,
132
118
  toolCalls?: MessageToolCall[],
133
119
  ) => Promise<void>;
120
+ /**
121
+ * update the message error with optimistic update
122
+ */
134
123
  internal_updateMessageError: (id: string, error: ChatMessageError | null) => Promise<void>;
124
+ /**
125
+ * create a message with optimistic update
126
+ */
135
127
  internal_createMessage: (
136
128
  params: CreateMessageParams,
137
129
  context?: { tempMessageId?: string; skipRefresh?: boolean },
138
130
  ) => Promise<string>;
131
+ /**
132
+ * create a temp message for optimistic update
133
+ * otherwise the message will be too slow to show
134
+ */
139
135
  internal_createTmpMessage: (params: CreateMessageParams) => string;
140
- internal_fetchMessages: () => Promise<void>;
136
+ /**
137
+ * delete the message content with optimistic update
138
+ */
139
+ internal_deleteMessage: (id: string) => Promise<void>;
141
140
  internal_resendMessage: (id: string, traceId?: string) => Promise<void>;
141
+
142
+ internal_fetchMessages: () => Promise<void>;
142
143
  internal_traceMessage: (id: string, payload: TraceEventPayloads) => Promise<void>;
144
+
145
+ /**
146
+ * method to toggle message create loading state
147
+ * the AI message status is creating -> generating
148
+ * other message role like user and tool , only this method need to be called
149
+ */
150
+ internal_toggleMessageLoading: (loading: boolean, id: string) => void;
151
+ /**
152
+ * method to toggle ai message generating loading
153
+ */
154
+ internal_toggleChatLoading: (
155
+ loading: boolean,
156
+ id?: string,
157
+ action?: string,
158
+ ) => AbortController | undefined;
159
+ /**
160
+ * method to toggle the tool calling loading state
161
+ */
162
+ internal_toggleToolCallingStreaming: (id: string, streaming: boolean[] | undefined) => void;
163
+ /**
164
+ * helper to toggle the loading state of the array,used by these three toggleXXXLoading
165
+ */
166
+ internal_toggleLoadingArrays: (
167
+ key: keyof ChatStoreState,
168
+ loading: boolean,
169
+ id?: string,
170
+ action?: string,
171
+ ) => AbortController | undefined;
143
172
  }
144
173
 
145
174
  const getAgentConfig = () => agentSelectors.currentAgentConfig(useAgentStore.getState());
@@ -155,30 +184,42 @@ export const chatMessage: StateCreator<
155
184
  const message = chatSelectors.getMessageById(id)(get());
156
185
  if (!message) return;
157
186
 
158
- const deleteFn = async (id: string) => {
159
- get().internal_dispatchMessage({ type: 'deleteMessage', id });
160
- await messageService.removeMessage(id);
161
- };
187
+ let ids = [message.id];
162
188
 
163
189
  // if the message is a tool calls, then delete all the related messages
164
- // TODO: maybe we need to delete it in the DB?
165
190
  if (message.tools) {
166
- const pools = message.tools
167
- .flatMap((tool) => {
168
- const messages = chatSelectors
169
- .currentChats(get())
170
- .filter((m) => m.tool_call_id === tool.id);
171
-
172
- return messages.map((m) => m.id);
173
- })
174
- .map((i) => deleteFn(i));
191
+ const toolMessageIds = message.tools.flatMap((tool) => {
192
+ const messages = chatSelectors
193
+ .currentChats(get())
194
+ .filter((m) => m.tool_call_id === tool.id);
175
195
 
176
- await Promise.all(pools);
196
+ return messages.map((m) => m.id);
197
+ });
198
+ ids = ids.concat(toolMessageIds);
177
199
  }
178
200
 
179
- await deleteFn(id);
201
+ get().internal_dispatchMessage({ type: 'deleteMessages', ids });
202
+ await messageService.removeMessages(ids);
180
203
  await get().refreshMessages();
181
204
  },
205
+
206
+ deleteToolMessage: async (id) => {
207
+ const message = chatSelectors.getMessageById(id)(get());
208
+ if (!message || message.role !== 'tool') return;
209
+
210
+ const removeToolInAssistantMessage = async () => {
211
+ if (!message.parentId) return;
212
+ await get().internal_removeToolToAssistantMessage(message.parentId, message.tool_call_id);
213
+ };
214
+
215
+ await Promise.all([
216
+ // 1. remove tool message
217
+ get().internal_deleteMessage(id),
218
+ // 2. remove the tool item in the assistant tools
219
+ removeToolInAssistantMessage(),
220
+ ]);
221
+ },
222
+
182
223
  delAndRegenerateMessage: async (id) => {
183
224
  const traceId = chatSelectors.getTraceIdByMessageId(id)(get());
184
225
  get().internal_resendMessage(id, traceId);
@@ -197,7 +238,7 @@ export const chatMessage: StateCreator<
197
238
  clearMessage: async () => {
198
239
  const { activeId, activeTopicId, refreshMessages, refreshTopic, switchTopic } = get();
199
240
 
200
- await messageService.removeMessages(activeId, activeTopicId);
241
+ await messageService.removeMessagesByAssistant(activeId, activeTopicId);
201
242
 
202
243
  if (activeTopicId) {
203
244
  await topicService.removeTopic(activeTopicId);
@@ -581,34 +622,7 @@ export const chatMessage: StateCreator<
581
622
  traceId: msgTraceId,
582
623
  };
583
624
  },
584
- internal_toggleChatLoading: (loading, id, action) => {
585
- return get().internal_toggleLoadingArrays('chatLoadingIds', loading, id, action);
586
- },
587
- internal_toggleMessageLoading: (loading, id) => {
588
- set(
589
- {
590
- messageLoadingIds: toggleBooleanList(get().messageLoadingIds, id, loading),
591
- },
592
- false,
593
- 'internal_toggleMessageLoading',
594
- );
595
- },
596
- internal_toggleToolCallingStreaming: (id, streaming) => {
597
- set(
598
- {
599
- toolCallingStreamIds: produce(get().toolCallingStreamIds, (draft) => {
600
- if (!!streaming) {
601
- draft[id] = streaming;
602
- } else {
603
- delete draft[id];
604
- }
605
- }),
606
- },
607
625
 
608
- false,
609
- 'toggleToolCallingStreaming',
610
- );
611
- },
612
626
  internal_resendMessage: async (messageId, traceId) => {
613
627
  // 1. 构造所有相关的历史记录
614
628
  const chats = chatSelectors.currentChats(get());
@@ -715,7 +729,11 @@ export const chatMessage: StateCreator<
715
729
 
716
730
  return tempId;
717
731
  },
718
-
732
+ internal_deleteMessage: async (id: string) => {
733
+ get().internal_dispatchMessage({ type: 'deleteMessage', id });
734
+ await messageService.removeMessage(id);
735
+ await get().refreshMessages();
736
+ },
719
737
  internal_traceMessage: async (id, payload) => {
720
738
  // tracing the diff of update
721
739
  const message = chatSelectors.getMessageById(id)(get());
@@ -731,6 +749,35 @@ export const chatMessage: StateCreator<
731
749
  }
732
750
  },
733
751
 
752
+ // ----- Loading ------- //
753
+ internal_toggleMessageLoading: (loading, id) => {
754
+ set(
755
+ {
756
+ messageLoadingIds: toggleBooleanList(get().messageLoadingIds, id, loading),
757
+ },
758
+ false,
759
+ 'internal_toggleMessageLoading',
760
+ );
761
+ },
762
+ internal_toggleChatLoading: (loading, id, action) => {
763
+ return get().internal_toggleLoadingArrays('chatLoadingIds', loading, id, action);
764
+ },
765
+ internal_toggleToolCallingStreaming: (id, streaming) => {
766
+ set(
767
+ {
768
+ toolCallingStreamIds: produce(get().toolCallingStreamIds, (draft) => {
769
+ if (!!streaming) {
770
+ draft[id] = streaming;
771
+ } else {
772
+ delete draft[id];
773
+ }
774
+ }),
775
+ },
776
+
777
+ false,
778
+ 'toggleToolCallingStreaming',
779
+ );
780
+ },
734
781
  internal_toggleLoadingArrays: (key, loading, id, action) => {
735
782
  if (loading) {
736
783
  window.addEventListener('beforeunload', preventLeavingFn);
@@ -1,4 +1,4 @@
1
- import { ChatMessage } from '@/types/message';
1
+ import { ChatMessage, ChatToolPayload } from '@/types/message';
2
2
 
3
3
  import { MessageDispatch, messagesReducer } from './reducer';
4
4
 
@@ -97,6 +97,35 @@ describe('messagesReducer', () => {
97
97
  expect(updatedMessage?.updatedAt).toBeGreaterThan(initialState[0].updatedAt);
98
98
  });
99
99
 
100
+ it('should update the extra field of a message if extra exist', () => {
101
+ const payload: MessageDispatch = {
102
+ type: 'updateMessageExtra',
103
+ id: 'data',
104
+ key: 'abc',
105
+ value: '2',
106
+ };
107
+
108
+ const newState = messagesReducer(
109
+ [
110
+ {
111
+ id: 'data',
112
+ content: 'Hello World',
113
+ createdAt: 1629264000000,
114
+ updatedAt: 1629264000000,
115
+ role: 'user',
116
+ meta: {},
117
+ extra: { abc: '1' },
118
+ } as ChatMessage,
119
+ ...initialState,
120
+ ],
121
+ payload,
122
+ );
123
+ const updatedMessage = newState.find((m) => m.id === 'data');
124
+
125
+ expect(updatedMessage?.extra).toEqual({ abc: '2' });
126
+ expect(updatedMessage?.updatedAt).toBeGreaterThan(initialState[0].updatedAt);
127
+ });
128
+
100
129
  it('should not modify state if message is not found', () => {
101
130
  const payload: MessageDispatch = {
102
131
  type: 'updateMessageExtra',
@@ -255,6 +284,151 @@ describe('messagesReducer', () => {
255
284
  });
256
285
  });
257
286
 
287
+ describe('addMessageTool', () => {
288
+ it('should add a tool to the specified assistant message if it dont have tools', () => {
289
+ const messageId = '1';
290
+ const toolPayload: ChatToolPayload = {
291
+ id: 'tc_1',
292
+ type: 'default',
293
+ identifier: 'tool1',
294
+ apiName: 'testFunction',
295
+ arguments: '{"arg1": "value1"}',
296
+ };
297
+
298
+ const payload: MessageDispatch = {
299
+ type: 'addMessageTool',
300
+ id: messageId,
301
+ value: toolPayload,
302
+ };
303
+
304
+ const newState = messagesReducer(
305
+ [...initialState, { id: messageId, role: 'assistant', content: '' } as ChatMessage],
306
+ payload,
307
+ );
308
+ const updatedMessage = newState.find((m) => m.id === messageId);
309
+
310
+ expect(updatedMessage).not.toBeUndefined();
311
+ expect(updatedMessage?.tools).toHaveLength(1);
312
+ expect(updatedMessage?.tools?.[0]).toEqual(toolPayload);
313
+ expect(updatedMessage?.updatedAt).toBeGreaterThan(initialState[0].updatedAt);
314
+ });
315
+
316
+ it('should add a tool to the specified assistant message', () => {
317
+ const messageId = 'message2';
318
+ const toolPayload: ChatToolPayload = {
319
+ id: 'tc_1',
320
+ type: 'default',
321
+ identifier: 'tool1',
322
+ apiName: 'testFunction',
323
+ arguments: '{"arg1": "value1"}',
324
+ };
325
+
326
+ const payload: MessageDispatch = {
327
+ type: 'addMessageTool',
328
+ id: messageId,
329
+ value: toolPayload,
330
+ };
331
+
332
+ const newState = messagesReducer(
333
+ [...initialState, { id: messageId, role: 'assistant', content: '' } as ChatMessage],
334
+ payload,
335
+ );
336
+ const updatedMessage = newState.find((m) => m.id === messageId);
337
+
338
+ expect(updatedMessage).not.toBeUndefined();
339
+ expect(updatedMessage?.tools).toHaveLength(2);
340
+ expect(updatedMessage?.tools?.[1]).toEqual(toolPayload);
341
+ });
342
+
343
+ it('should not modify the state if the message is not found', () => {
344
+ const toolPayload: ChatToolPayload = {
345
+ id: 'tc_1',
346
+ type: 'default',
347
+ identifier: 'tool1',
348
+ apiName: 'testFunction',
349
+ arguments: '{"arg1": "value1"}',
350
+ };
351
+
352
+ const payload: MessageDispatch = {
353
+ type: 'addMessageTool',
354
+ id: 'nonexistentMessage',
355
+ value: toolPayload,
356
+ };
357
+
358
+ const newState = messagesReducer(initialState, payload);
359
+ expect(newState).toEqual(initialState);
360
+ });
361
+
362
+ it('should not add a tool if the message is not an assistant message', () => {
363
+ const toolPayload: ChatToolPayload = {
364
+ id: 'tc_1',
365
+ type: 'default',
366
+ identifier: 'tool1',
367
+ apiName: 'testFunction',
368
+ arguments: '{"arg1": "value1"}',
369
+ };
370
+
371
+ const payload: MessageDispatch = {
372
+ type: 'addMessageTool',
373
+ id: 'message1', // This is a user message
374
+ value: toolPayload,
375
+ };
376
+
377
+ const newState = messagesReducer(initialState, payload);
378
+ expect(newState).toEqual(initialState);
379
+ });
380
+ });
381
+
382
+ describe('deleteMessageTool', () => {
383
+ it('should delete the specified tool from the message', () => {
384
+ const payload: MessageDispatch = {
385
+ type: 'deleteMessageTool',
386
+ id: 'message2',
387
+ tool_call_id: 'abc',
388
+ };
389
+
390
+ const newState = messagesReducer(initialState, payload);
391
+ const updatedMessage = newState.find((m) => m.id === 'message2');
392
+
393
+ expect(updatedMessage).not.toBeUndefined();
394
+ expect(updatedMessage?.tools).toHaveLength(0);
395
+ expect(updatedMessage?.updatedAt).toBeGreaterThan(initialState[0].updatedAt);
396
+ });
397
+
398
+ it('should not modify the state if the message is not found', () => {
399
+ const payload: MessageDispatch = {
400
+ type: 'deleteMessageTool',
401
+ id: 'nonexistentMessage',
402
+ tool_call_id: 'tool1',
403
+ };
404
+
405
+ const newState = messagesReducer(initialState, payload);
406
+ expect(newState).toEqual(initialState);
407
+ });
408
+
409
+ it('should not modify the state if the tool is not found', () => {
410
+ const payload: MessageDispatch = {
411
+ type: 'deleteMessageTool',
412
+ id: 'message1',
413
+ tool_call_id: 'nonexistentTool',
414
+ };
415
+
416
+ const newState = messagesReducer(initialState, payload);
417
+ expect(newState).toEqual(initialState);
418
+ });
419
+
420
+ it('should not delete a tool if the message is not an assistant message', () => {
421
+ const payload: MessageDispatch = {
422
+ type: 'deleteMessageTool',
423
+ id: 'message1', // This is a user message
424
+ tool_call_id: 'tool1',
425
+ };
426
+
427
+ const newState = messagesReducer(initialState, payload);
428
+ expect(newState).toEqual(initialState);
429
+ });
430
+ });
431
+
258
432
  describe('createMessage', () => {
259
433
  it('should add a new message to the state', () => {
260
434
  const payload: MessageDispatch = {
@@ -303,4 +477,29 @@ describe('messagesReducer', () => {
303
477
  expect(newState).toEqual(initialState);
304
478
  });
305
479
  });
480
+
481
+ describe('deleteMessages', () => {
482
+ it('should remove 2 messages from the state', () => {
483
+ const payload: MessageDispatch = {
484
+ type: 'deleteMessages',
485
+ ids: ['message1', 'message2'],
486
+ };
487
+
488
+ const newState = messagesReducer(initialState, payload);
489
+
490
+ expect(newState.length).toBe(0);
491
+ expect(newState.find((m) => m.id === 'message1')).toBeUndefined();
492
+ expect(newState.find((m) => m.id === 'message2')).toBeUndefined();
493
+ });
494
+
495
+ it('should not modify state if message to delete is not found', () => {
496
+ const payload: MessageDispatch = {
497
+ type: 'deleteMessage',
498
+ id: 'nonexistentMessage',
499
+ };
500
+
501
+ const newState = messagesReducer(initialState, payload);
502
+ expect(newState).toEqual(initialState);
503
+ });
504
+ });
306
505
  });