@lobehub/lobehub 2.0.0-next.84 → 2.0.0-next.86

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 (89) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/apps/desktop/src/main/modules/networkProxy/dispatcher.ts +16 -16
  3. package/apps/desktop/src/main/modules/networkProxy/tester.ts +11 -11
  4. package/apps/desktop/src/main/modules/networkProxy/urlBuilder.ts +3 -3
  5. package/apps/desktop/src/main/modules/networkProxy/validator.ts +10 -10
  6. package/changelog/v1.json +18 -0
  7. package/package.json +1 -1
  8. package/packages/agent-runtime/src/core/runtime.ts +36 -1
  9. package/packages/agent-runtime/src/types/event.ts +1 -0
  10. package/packages/agent-runtime/src/types/generalAgent.ts +16 -0
  11. package/packages/agent-runtime/src/types/instruction.ts +30 -0
  12. package/packages/agent-runtime/src/types/runtime.ts +7 -0
  13. package/packages/types/src/message/common/metadata.ts +3 -0
  14. package/packages/types/src/message/common/tools.ts +2 -2
  15. package/packages/types/src/tool/search/index.ts +8 -2
  16. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/index.tsx +2 -2
  17. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/useSend.ts +7 -2
  18. package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend.ts +15 -14
  19. package/src/app/[variants]/(main)/chat/session/features/SessionListContent/List/Item/index.tsx +2 -2
  20. package/src/app/[variants]/(main)/discover/(list)/features/Pagination.tsx +1 -1
  21. package/src/features/ChatInput/ActionBar/STT/browser.tsx +2 -2
  22. package/src/features/ChatInput/ActionBar/STT/openai.tsx +2 -2
  23. package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -1
  24. package/src/features/Conversation/Messages/User/index.tsx +3 -3
  25. package/src/features/Conversation/Messages/index.tsx +3 -3
  26. package/src/features/Conversation/components/AutoScroll.tsx +2 -2
  27. package/src/services/search.ts +2 -2
  28. package/src/store/chat/agents/GeneralChatAgent.ts +98 -0
  29. package/src/store/chat/agents/__tests__/GeneralChatAgent.test.ts +366 -0
  30. package/src/store/chat/agents/__tests__/createAgentExecutors/call-llm.test.ts +1217 -0
  31. package/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts +1976 -0
  32. package/src/store/chat/agents/__tests__/createAgentExecutors/finish.test.ts +453 -0
  33. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/index.ts +4 -0
  34. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockInstructions.ts +126 -0
  35. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockMessages.ts +94 -0
  36. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockOperations.ts +96 -0
  37. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockStore.ts +138 -0
  38. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/assertions.ts +185 -0
  39. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/index.ts +3 -0
  40. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/operationTestUtils.ts +94 -0
  41. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/testExecutor.ts +139 -0
  42. package/src/store/chat/agents/__tests__/createAgentExecutors/request-human-approve.test.ts +545 -0
  43. package/src/store/chat/agents/__tests__/createAgentExecutors/resolve-aborted-tools.test.ts +686 -0
  44. package/src/store/chat/agents/createAgentExecutors.ts +313 -80
  45. package/src/store/chat/selectors.ts +1 -0
  46. package/src/store/chat/slices/aiChat/__tests__/ai-chat.integration.test.ts +667 -0
  47. package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +137 -27
  48. package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +163 -125
  49. package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +12 -2
  50. package/src/store/chat/slices/aiChat/actions/__tests__/fixtures.ts +0 -2
  51. package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +0 -2
  52. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +286 -19
  53. package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +0 -112
  54. package/src/store/chat/slices/aiChat/actions/conversationControl.ts +42 -99
  55. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +90 -57
  56. package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +5 -25
  57. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +220 -98
  58. package/src/store/chat/slices/aiChat/actions/streamingStates.ts +0 -34
  59. package/src/store/chat/slices/aiChat/initialState.ts +0 -28
  60. package/src/store/chat/slices/aiChat/selectors.test.ts +280 -0
  61. package/src/store/chat/slices/aiChat/selectors.ts +31 -7
  62. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +21 -30
  63. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +29 -49
  64. package/src/store/chat/slices/builtinTool/actions/interpreter.ts +83 -48
  65. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +78 -28
  66. package/src/store/chat/slices/builtinTool/actions/search.ts +146 -59
  67. package/src/store/chat/slices/builtinTool/selectors.test.ts +258 -0
  68. package/src/store/chat/slices/builtinTool/selectors.ts +25 -4
  69. package/src/store/chat/slices/message/action.test.ts +134 -16
  70. package/src/store/chat/slices/message/actions/internals.ts +33 -7
  71. package/src/store/chat/slices/message/actions/optimisticUpdate.ts +85 -52
  72. package/src/store/chat/slices/message/initialState.ts +0 -10
  73. package/src/store/chat/slices/message/selectors/messageState.ts +34 -12
  74. package/src/store/chat/slices/operation/__tests__/actions.test.ts +712 -16
  75. package/src/store/chat/slices/operation/__tests__/integration.test.ts +342 -0
  76. package/src/store/chat/slices/operation/__tests__/selectors.test.ts +257 -17
  77. package/src/store/chat/slices/operation/actions.ts +218 -11
  78. package/src/store/chat/slices/operation/selectors.ts +135 -6
  79. package/src/store/chat/slices/operation/types.ts +29 -3
  80. package/src/store/chat/slices/plugin/action.test.ts +30 -322
  81. package/src/store/chat/slices/plugin/actions/internals.ts +0 -14
  82. package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +21 -19
  83. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +45 -27
  84. package/src/store/chat/slices/plugin/actions/publicApi.ts +3 -4
  85. package/src/store/chat/slices/plugin/actions/workflow.ts +0 -55
  86. package/src/store/chat/slices/thread/selectors/index.ts +4 -2
  87. package/src/store/chat/slices/translate/action.ts +54 -41
  88. package/src/tools/web-browsing/ExecutionRuntime/index.ts +5 -2
  89. package/src/tools/web-browsing/Portal/Search/Footer.tsx +11 -9
@@ -0,0 +1,280 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import { beforeEach, describe, expect, it } from 'vitest';
3
+
4
+ import { useChatStore } from '@/store/chat/store';
5
+
6
+ import { aiChatSelectors } from './selectors';
7
+
8
+ describe('aiChatSelectors', () => {
9
+ beforeEach(() => {
10
+ useChatStore.setState(useChatStore.getInitialState());
11
+ });
12
+
13
+ describe('isMessageInReasoning', () => {
14
+ it('should return true when message has reasoning operation', () => {
15
+ const { result } = renderHook(() => useChatStore());
16
+
17
+ act(() => {
18
+ useChatStore.setState({ activeId: 'session1', activeTopicId: 'topic1' });
19
+ result.current.startOperation({
20
+ type: 'reasoning',
21
+ context: { sessionId: 'session1', topicId: 'topic1', messageId: 'msg1' },
22
+ });
23
+ });
24
+
25
+ expect(aiChatSelectors.isMessageInReasoning('msg1')(result.current)).toBe(true);
26
+ });
27
+
28
+ it('should return false when message has no reasoning operation', () => {
29
+ const { result } = renderHook(() => useChatStore());
30
+
31
+ expect(aiChatSelectors.isMessageInReasoning('msg1')(result.current)).toBe(false);
32
+ });
33
+ });
34
+
35
+ describe('isMessageInSearchWorkflow', () => {
36
+ it('should return true when message is in search workflow', () => {
37
+ const { result } = renderHook(() => useChatStore());
38
+
39
+ act(() => {
40
+ useChatStore.setState({ searchWorkflowLoadingIds: ['msg1', 'msg2'] });
41
+ });
42
+
43
+ expect(aiChatSelectors.isMessageInSearchWorkflow('msg1')(result.current)).toBe(true);
44
+ expect(aiChatSelectors.isMessageInSearchWorkflow('msg2')(result.current)).toBe(true);
45
+ });
46
+
47
+ it('should return false when message is not in search workflow', () => {
48
+ const { result } = renderHook(() => useChatStore());
49
+
50
+ act(() => {
51
+ useChatStore.setState({ searchWorkflowLoadingIds: ['msg1'] });
52
+ });
53
+
54
+ expect(aiChatSelectors.isMessageInSearchWorkflow('msg2')(result.current)).toBe(false);
55
+ });
56
+ });
57
+
58
+ describe('isIntentUnderstanding', () => {
59
+ it('should return true when message is in search workflow', () => {
60
+ const { result } = renderHook(() => useChatStore());
61
+
62
+ act(() => {
63
+ useChatStore.setState({ searchWorkflowLoadingIds: ['msg1'] });
64
+ });
65
+
66
+ expect(aiChatSelectors.isIntentUnderstanding('msg1')(result.current)).toBe(true);
67
+ });
68
+
69
+ it('should return false when message is not in search workflow', () => {
70
+ const { result } = renderHook(() => useChatStore());
71
+
72
+ expect(aiChatSelectors.isIntentUnderstanding('msg1')(result.current)).toBe(false);
73
+ });
74
+ });
75
+
76
+ describe('isCurrentSendMessageLoading', () => {
77
+ it('should return true when there is a running sendMessage operation in current context', () => {
78
+ const { result } = renderHook(() => useChatStore());
79
+
80
+ act(() => {
81
+ useChatStore.setState({ activeId: 'session1', activeTopicId: 'topic1' });
82
+ result.current.startOperation({
83
+ type: 'sendMessage',
84
+ context: { sessionId: 'session1', topicId: 'topic1' },
85
+ });
86
+ });
87
+
88
+ expect(aiChatSelectors.isCurrentSendMessageLoading(result.current)).toBe(true);
89
+ });
90
+
91
+ it('should return false when there is no sendMessage operation', () => {
92
+ const { result } = renderHook(() => useChatStore());
93
+
94
+ act(() => {
95
+ useChatStore.setState({ activeId: 'session1', activeTopicId: 'topic1' });
96
+ });
97
+
98
+ expect(aiChatSelectors.isCurrentSendMessageLoading(result.current)).toBe(false);
99
+ });
100
+
101
+ it('should return false when sendMessage operation is completed', () => {
102
+ const { result } = renderHook(() => useChatStore());
103
+
104
+ let opId: string;
105
+
106
+ act(() => {
107
+ useChatStore.setState({ activeId: 'session1', activeTopicId: 'topic1' });
108
+ opId = result.current.startOperation({
109
+ type: 'sendMessage',
110
+ context: { sessionId: 'session1', topicId: 'topic1' },
111
+ }).operationId;
112
+ });
113
+
114
+ act(() => {
115
+ result.current.completeOperation(opId);
116
+ });
117
+
118
+ expect(aiChatSelectors.isCurrentSendMessageLoading(result.current)).toBe(false);
119
+ });
120
+
121
+ it('should return false for different context', () => {
122
+ const { result } = renderHook(() => useChatStore());
123
+
124
+ act(() => {
125
+ useChatStore.setState({ activeId: 'session1', activeTopicId: 'topic1' });
126
+ result.current.startOperation({
127
+ type: 'sendMessage',
128
+ context: { sessionId: 'session2', topicId: 'topic2' },
129
+ });
130
+ });
131
+
132
+ expect(aiChatSelectors.isCurrentSendMessageLoading(result.current)).toBe(false);
133
+ });
134
+ });
135
+
136
+ describe('isCurrentSendMessageError', () => {
137
+ it('should return error message when latest sendMessage operation has error', () => {
138
+ const { result } = renderHook(() => useChatStore());
139
+
140
+ let opId: string;
141
+
142
+ act(() => {
143
+ useChatStore.setState({ activeId: 'session1', activeTopicId: 'topic1' });
144
+ opId = result.current.startOperation({
145
+ type: 'sendMessage',
146
+ context: { sessionId: 'session1', topicId: 'topic1' },
147
+ }).operationId;
148
+ });
149
+
150
+ act(() => {
151
+ result.current.updateOperationMetadata(opId, {
152
+ inputSendErrorMsg: 'Network error',
153
+ });
154
+ });
155
+
156
+ expect(aiChatSelectors.isCurrentSendMessageError(result.current)).toBe('Network error');
157
+ });
158
+
159
+ it('should return undefined when there is no error', () => {
160
+ const { result } = renderHook(() => useChatStore());
161
+
162
+ act(() => {
163
+ useChatStore.setState({ activeId: 'session1', activeTopicId: 'topic1' });
164
+ result.current.startOperation({
165
+ type: 'sendMessage',
166
+ context: { sessionId: 'session1', topicId: 'topic1' },
167
+ });
168
+ });
169
+
170
+ expect(aiChatSelectors.isCurrentSendMessageError(result.current)).toBeUndefined();
171
+ });
172
+
173
+ it('should return undefined when there are no operations', () => {
174
+ const { result } = renderHook(() => useChatStore());
175
+
176
+ act(() => {
177
+ useChatStore.setState({ activeId: 'session1', activeTopicId: 'topic1' });
178
+ });
179
+
180
+ expect(aiChatSelectors.isCurrentSendMessageError(result.current)).toBeUndefined();
181
+ });
182
+
183
+ it('should return the latest error when multiple operations exist', () => {
184
+ const { result } = renderHook(() => useChatStore());
185
+
186
+ let op1Id: string;
187
+ let op2Id: string;
188
+
189
+ act(() => {
190
+ useChatStore.setState({ activeId: 'session1', activeTopicId: 'topic1' });
191
+
192
+ op1Id = result.current.startOperation({
193
+ type: 'sendMessage',
194
+ context: { sessionId: 'session1', topicId: 'topic1' },
195
+ }).operationId;
196
+
197
+ op2Id = result.current.startOperation({
198
+ type: 'sendMessage',
199
+ context: { sessionId: 'session1', topicId: 'topic1' },
200
+ }).operationId;
201
+ });
202
+
203
+ act(() => {
204
+ result.current.updateOperationMetadata(op1Id, {
205
+ inputSendErrorMsg: 'First error',
206
+ });
207
+ result.current.updateOperationMetadata(op2Id, {
208
+ inputSendErrorMsg: 'Second error',
209
+ });
210
+ });
211
+
212
+ // Should return the latest (second) error
213
+ expect(aiChatSelectors.isCurrentSendMessageError(result.current)).toBe('Second error');
214
+ });
215
+ });
216
+
217
+ describe('isSendMessageLoadingForTopic', () => {
218
+ it('should return true when sendMessage operation is running for the topic', () => {
219
+ const { result } = renderHook(() => useChatStore());
220
+
221
+ act(() => {
222
+ result.current.startOperation({
223
+ type: 'sendMessage',
224
+ context: { sessionId: 'session1', topicId: 'topic1' },
225
+ });
226
+ });
227
+
228
+ expect(aiChatSelectors.isSendMessageLoadingForTopic('session1_topic1')(result.current)).toBe(
229
+ true,
230
+ );
231
+ });
232
+
233
+ it('should return false when no sendMessage operation exists', () => {
234
+ const { result } = renderHook(() => useChatStore());
235
+
236
+ expect(aiChatSelectors.isSendMessageLoadingForTopic('session1_topic1')(result.current)).toBe(
237
+ false,
238
+ );
239
+ });
240
+
241
+ it('should return false when sendMessage operation is completed', () => {
242
+ const { result } = renderHook(() => useChatStore());
243
+
244
+ let opId: string;
245
+
246
+ act(() => {
247
+ opId = result.current.startOperation({
248
+ type: 'sendMessage',
249
+ context: { sessionId: 'session1', topicId: 'topic1' },
250
+ }).operationId;
251
+ });
252
+
253
+ act(() => {
254
+ result.current.completeOperation(opId);
255
+ });
256
+
257
+ expect(aiChatSelectors.isSendMessageLoadingForTopic('session1_topic1')(result.current)).toBe(
258
+ false,
259
+ );
260
+ });
261
+
262
+ it('should distinguish between different topics', () => {
263
+ const { result } = renderHook(() => useChatStore());
264
+
265
+ act(() => {
266
+ result.current.startOperation({
267
+ type: 'sendMessage',
268
+ context: { sessionId: 'session1', topicId: 'topic1' },
269
+ });
270
+ });
271
+
272
+ expect(aiChatSelectors.isSendMessageLoadingForTopic('session1_topic1')(result.current)).toBe(
273
+ true,
274
+ );
275
+ expect(aiChatSelectors.isSendMessageLoadingForTopic('session1_topic2')(result.current)).toBe(
276
+ false,
277
+ );
278
+ });
279
+ });
280
+ });
@@ -1,9 +1,10 @@
1
1
  import { messageMapKey } from '@/store/chat/utils/messageMapKey';
2
2
 
3
3
  import type { ChatStoreState } from '../../initialState';
4
+ import { operationSelectors } from '../operation/selectors';
4
5
 
5
6
  const isMessageInReasoning = (id: string) => (s: ChatStoreState) =>
6
- s.reasoningLoadingIds.includes(id);
7
+ operationSelectors.isMessageInReasoning(id)(s);
7
8
 
8
9
  const isMessageInSearchWorkflow = (id: string) => (s: ChatStoreState) =>
9
10
  s.searchWorkflowLoadingIds.includes(id);
@@ -12,17 +13,40 @@ const isIntentUnderstanding = (id: string) => (s: ChatStoreState) =>
12
13
  isMessageInSearchWorkflow(id)(s);
13
14
 
14
15
  const isCurrentSendMessageLoading = (s: ChatStoreState) => {
15
- const operationKey = messageMapKey(s.activeId, s.activeTopicId);
16
- return s.mainSendMessageOperations[operationKey]?.isLoading || false;
16
+ const contextKey = messageMapKey(s.activeId, s.activeTopicId);
17
+ const operationIds = s.operationsByContext[contextKey] || [];
18
+
19
+ // Check if there's any running sendMessage operation
20
+ return operationIds.some((opId) => {
21
+ const op = s.operations[opId];
22
+ return op && op.type === 'sendMessage' && op.status === 'running';
23
+ });
17
24
  };
18
25
 
19
26
  const isCurrentSendMessageError = (s: ChatStoreState) => {
20
- const operationKey = messageMapKey(s.activeId, s.activeTopicId);
21
- return s.mainSendMessageOperations[operationKey]?.inputSendErrorMsg;
27
+ const contextKey = messageMapKey(s.activeId, s.activeTopicId);
28
+ const operationIds = s.operationsByContext[contextKey] || [];
29
+
30
+ // Find the latest sendMessage operation with error
31
+ for (const opId of [...operationIds].reverse()) {
32
+ const op = s.operations[opId];
33
+ if (op && op.type === 'sendMessage' && op.metadata.inputSendErrorMsg) {
34
+ return op.metadata.inputSendErrorMsg;
35
+ }
36
+ }
37
+
38
+ return undefined;
22
39
  };
23
40
 
24
- const isSendMessageLoadingForTopic = (topicKey: string) => (s: ChatStoreState) =>
25
- s.mainSendMessageOperations[topicKey]?.isLoading ?? false;
41
+ const isSendMessageLoadingForTopic = (topicKey: string) => (s: ChatStoreState) => {
42
+ const operationIds = s.operationsByContext[topicKey] || [];
43
+
44
+ // Check if there's any running sendMessage operation for this topic
45
+ return operationIds.some((opId) => {
46
+ const op = s.operations[opId];
47
+ return op && op.type === 'sendMessage' && op.status === 'running';
48
+ });
49
+ };
26
50
 
27
51
  export const aiChatSelectors = {
28
52
  isCurrentSendMessageError,
@@ -21,13 +21,19 @@ vi.mock('@/services/electron/localFileService', () => ({
21
21
  const mockSet = vi.fn();
22
22
 
23
23
  const mockStore = {
24
+ completeOperation: vi.fn(),
25
+ failOperation: vi.fn(),
24
26
  internal_triggerLocalFileToolCalling: vi.fn(),
27
+ messageOperationMap: {},
25
28
  optimisticUpdateMessageContent: vi.fn(),
26
29
  optimisticUpdateMessagePluginError: vi.fn(),
27
30
  optimisticUpdatePluginArguments: vi.fn(),
28
31
  optimisticUpdatePluginState: vi.fn(),
29
32
  set: mockSet,
30
- toggleLocalFileLoading: vi.fn(),
33
+ startOperation: vi.fn().mockReturnValue({
34
+ abortController: new AbortController(),
35
+ operationId: 'test-op-id',
36
+ }),
31
37
  } as unknown as ChatStore;
32
38
 
33
39
  const createStore = () => {
@@ -58,10 +64,10 @@ describe('localFileSlice', () => {
58
64
 
59
65
  await store.internal_triggerLocalFileToolCalling('test-id', mockService);
60
66
 
61
- expect(mockStore.toggleLocalFileLoading).toBeCalledWith('test-id', true);
62
- expect(mockStore.optimisticUpdatePluginState).toBeCalledWith('test-id', mockState);
63
- expect(mockStore.optimisticUpdateMessageContent).toBeCalledWith('test-id', mockContent);
64
- expect(mockStore.toggleLocalFileLoading).toBeCalledWith('test-id', false);
67
+ expect(mockStore.startOperation).toBeCalled();
68
+ expect(mockStore.completeOperation).toBeCalled();
69
+ expect(mockStore.optimisticUpdatePluginState).toBeCalled();
70
+ expect(mockStore.optimisticUpdateMessageContent).toBeCalled();
65
71
  });
66
72
 
67
73
  it('should handle error', async () => {
@@ -70,11 +76,15 @@ describe('localFileSlice', () => {
70
76
 
71
77
  await store.internal_triggerLocalFileToolCalling('test-id', mockService);
72
78
 
73
- expect(mockStore.optimisticUpdateMessagePluginError).toBeCalledWith('test-id', {
74
- body: mockError,
75
- message: 'test error',
76
- type: 'PluginServerError',
77
- });
79
+ expect(mockStore.optimisticUpdateMessagePluginError).toBeCalledWith(
80
+ 'test-id',
81
+ {
82
+ body: mockError,
83
+ message: 'test error',
84
+ type: 'PluginServerError',
85
+ },
86
+ { operationId: 'test-op-id' },
87
+ );
78
88
  });
79
89
  });
80
90
 
@@ -187,24 +197,5 @@ describe('localFileSlice', () => {
187
197
  });
188
198
  });
189
199
 
190
- describe('toggleLocalFileLoading', () => {
191
- it('should toggle loading state', () => {
192
- const mockSetFn = vi.fn();
193
- const testStore = localSystemSlice(mockSetFn, () => mockStore, {} as any);
194
-
195
- testStore.toggleLocalFileLoading('test-id', true);
196
- expect(mockSetFn).toHaveBeenCalledWith(
197
- expect.any(Function),
198
- false,
199
- 'toggleLocalFileLoading/start',
200
- );
201
-
202
- testStore.toggleLocalFileLoading('test-id', false);
203
- expect(mockSetFn).toHaveBeenCalledWith(
204
- expect.any(Function),
205
- false,
206
- 'toggleLocalFileLoading/end',
207
- );
208
- });
209
- });
200
+ // toggleLocalFileLoading is no longer needed as we use operation-based state management
210
201
  });
@@ -85,16 +85,18 @@ describe('search actions', () => {
85
85
  },
86
86
  ];
87
87
 
88
- expect(searchService.webSearch).toHaveBeenCalledWith({
89
- searchEngines: ['google'],
90
- query: 'test query',
91
- });
92
- expect(result.current.searchLoading[messageId]).toBe(false);
88
+ expect(searchService.webSearch).toHaveBeenCalledWith(
89
+ {
90
+ searchEngines: ['google'],
91
+ query: 'test query',
92
+ },
93
+ { signal: expect.any(AbortSignal) },
94
+ );
93
95
  expect(result.current.optimisticUpdateMessageContent).toHaveBeenCalledWith(
94
96
  messageId,
95
97
  searchResultsPrompt(expectedContent),
96
98
  undefined,
97
- { sessionId: undefined, topicId: undefined },
99
+ { operationId: expect.any(String) },
98
100
  );
99
101
  });
100
102
 
@@ -122,17 +124,19 @@ describe('search actions', () => {
122
124
  await search(messageId, query);
123
125
  });
124
126
 
125
- expect(searchService.webSearch).toHaveBeenCalledWith({
126
- searchEngines: ['custom-engine'],
127
- searchTimeRange: 'year',
128
- query: 'test query',
129
- });
130
- expect(result.current.searchLoading[messageId]).toBe(false);
127
+ expect(searchService.webSearch).toHaveBeenCalledWith(
128
+ {
129
+ searchEngines: ['custom-engine'],
130
+ searchTimeRange: 'year',
131
+ query: 'test query',
132
+ },
133
+ { signal: expect.any(AbortSignal) },
134
+ );
131
135
  expect(result.current.optimisticUpdateMessageContent).toHaveBeenCalledWith(
132
136
  messageId,
133
137
  searchResultsPrompt([]),
134
138
  undefined,
135
- { sessionId: undefined, topicId: undefined },
139
+ { operationId: expect.any(String) },
136
140
  );
137
141
  });
138
142
 
@@ -159,14 +163,7 @@ describe('search actions', () => {
159
163
  message: 'Search failed',
160
164
  type: 'PluginServerError',
161
165
  },
162
- { sessionId: undefined, topicId: undefined },
163
- );
164
- expect(result.current.searchLoading[messageId]).toBe(false);
165
- expect(result.current.optimisticUpdateMessageContent).toHaveBeenCalledWith(
166
- messageId,
167
- 'Search failed',
168
- undefined,
169
- { sessionId: undefined, topicId: undefined },
166
+ { operationId: expect.any(String) },
170
167
  );
171
168
  });
172
169
  });
@@ -207,7 +204,7 @@ describe('search actions', () => {
207
204
  messageId,
208
205
  crawlResultsPrompt(expectedContent as any),
209
206
  undefined,
210
- { sessionId: undefined, topicId: undefined },
207
+ { operationId: expect.any(String) },
211
208
  );
212
209
  });
213
210
 
@@ -235,7 +232,7 @@ describe('search actions', () => {
235
232
  messageId,
236
233
  crawlResultsPrompt(mockResponse.results),
237
234
  undefined,
238
- { sessionId: undefined, topicId: undefined },
235
+ { operationId: expect.any(String) },
239
236
  );
240
237
  });
241
238
  });
@@ -304,7 +301,7 @@ describe('search actions', () => {
304
301
  sessionId: 'session-id',
305
302
  topicId: 'topic-id',
306
303
  }),
307
- { sessionId: 'session-id', topicId: 'topic-id' },
304
+ { operationId: expect.any(String) },
308
305
  );
309
306
 
310
307
  expect(result.current.optimisticAddToolToAssistantMessage).toHaveBeenCalledWith(
@@ -313,7 +310,7 @@ describe('search actions', () => {
313
310
  identifier: 'search',
314
311
  type: 'default',
315
312
  }),
316
- { sessionId: undefined, topicId: undefined },
313
+ { operationId: expect.any(String) },
317
314
  );
318
315
  });
319
316
 
@@ -332,24 +329,7 @@ describe('search actions', () => {
332
329
  });
333
330
  });
334
331
 
335
- describe('toggleSearchLoading', () => {
336
- it('should toggle search loading state', () => {
337
- const { result } = renderHook(() => useChatStore());
338
- const messageId = 'test-message-id';
339
-
340
- act(() => {
341
- result.current.toggleSearchLoading(messageId, true);
342
- });
343
-
344
- expect(result.current.searchLoading[messageId]).toBe(true);
345
-
346
- act(() => {
347
- result.current.toggleSearchLoading(messageId, false);
348
- });
349
-
350
- expect(result.current.searchLoading[messageId]).toBe(false);
351
- });
352
- });
332
+ // toggleSearchLoading is no longer needed as we use operation-based state management
353
333
 
354
334
  describe('OptimisticUpdateContext isolation', () => {
355
335
  it('search should pass context to optimistic methods', async () => {
@@ -401,13 +381,13 @@ describe('search actions', () => {
401
381
  expect(result.current.optimisticUpdatePluginState).toHaveBeenCalledWith(
402
382
  messageId,
403
383
  expect.any(Object),
404
- { sessionId: contextSessionId, topicId: contextTopicId },
384
+ { operationId: expect.any(String) },
405
385
  );
406
386
  expect(result.current.optimisticUpdateMessageContent).toHaveBeenCalledWith(
407
387
  messageId,
408
388
  expect.any(String),
409
389
  undefined,
410
- { sessionId: contextSessionId, topicId: contextTopicId },
390
+ { operationId: expect.any(String) },
411
391
  );
412
392
  });
413
393
 
@@ -456,12 +436,12 @@ describe('search actions', () => {
456
436
  messageId,
457
437
  expect.any(String),
458
438
  undefined,
459
- { sessionId: contextSessionId, topicId: contextTopicId },
439
+ { operationId: expect.any(String) },
460
440
  );
461
441
  expect(result.current.optimisticUpdatePluginState).toHaveBeenCalledWith(
462
442
  messageId,
463
443
  expect.any(Object),
464
- { sessionId: contextSessionId, topicId: contextTopicId },
444
+ { operationId: expect.any(String) },
465
445
  );
466
446
  });
467
447
 
@@ -510,14 +490,14 @@ describe('search actions', () => {
510
490
  identifier: 'search',
511
491
  type: 'default',
512
492
  }),
513
- { sessionId: contextSessionId, topicId: contextTopicId },
493
+ { operationId: expect.any(String) },
514
494
  );
515
495
  expect(result.current.optimisticCreateMessage).toHaveBeenCalledWith(
516
496
  expect.objectContaining({
517
497
  sessionId: contextSessionId,
518
498
  topicId: contextTopicId,
519
499
  }),
520
- { sessionId: contextSessionId, topicId: contextTopicId },
500
+ { operationId: expect.any(String) },
521
501
  );
522
502
  });
523
503
  });