@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.
- package/CHANGELOG.md +50 -0
- package/apps/desktop/src/main/modules/networkProxy/dispatcher.ts +16 -16
- package/apps/desktop/src/main/modules/networkProxy/tester.ts +11 -11
- package/apps/desktop/src/main/modules/networkProxy/urlBuilder.ts +3 -3
- package/apps/desktop/src/main/modules/networkProxy/validator.ts +10 -10
- package/changelog/v1.json +18 -0
- package/package.json +1 -1
- package/packages/agent-runtime/src/core/runtime.ts +36 -1
- package/packages/agent-runtime/src/types/event.ts +1 -0
- package/packages/agent-runtime/src/types/generalAgent.ts +16 -0
- package/packages/agent-runtime/src/types/instruction.ts +30 -0
- package/packages/agent-runtime/src/types/runtime.ts +7 -0
- package/packages/types/src/message/common/metadata.ts +3 -0
- package/packages/types/src/message/common/tools.ts +2 -2
- package/packages/types/src/tool/search/index.ts +8 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/index.tsx +2 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/useSend.ts +7 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend.ts +15 -14
- package/src/app/[variants]/(main)/chat/session/features/SessionListContent/List/Item/index.tsx +2 -2
- package/src/app/[variants]/(main)/discover/(list)/features/Pagination.tsx +1 -1
- package/src/features/ChatInput/ActionBar/STT/browser.tsx +2 -2
- package/src/features/ChatInput/ActionBar/STT/openai.tsx +2 -2
- package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -1
- package/src/features/Conversation/Messages/User/index.tsx +3 -3
- package/src/features/Conversation/Messages/index.tsx +3 -3
- package/src/features/Conversation/components/AutoScroll.tsx +2 -2
- package/src/services/search.ts +2 -2
- package/src/store/chat/agents/GeneralChatAgent.ts +98 -0
- package/src/store/chat/agents/__tests__/GeneralChatAgent.test.ts +366 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/call-llm.test.ts +1217 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts +1976 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/finish.test.ts +453 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/index.ts +4 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockInstructions.ts +126 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockMessages.ts +94 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockOperations.ts +96 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockStore.ts +138 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/assertions.ts +185 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/index.ts +3 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/operationTestUtils.ts +94 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/testExecutor.ts +139 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/request-human-approve.test.ts +545 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/resolve-aborted-tools.test.ts +686 -0
- package/src/store/chat/agents/createAgentExecutors.ts +313 -80
- package/src/store/chat/selectors.ts +1 -0
- package/src/store/chat/slices/aiChat/__tests__/ai-chat.integration.test.ts +667 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +137 -27
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +163 -125
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +12 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/fixtures.ts +0 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +0 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +286 -19
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +0 -112
- package/src/store/chat/slices/aiChat/actions/conversationControl.ts +42 -99
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +90 -57
- package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +5 -25
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +220 -98
- package/src/store/chat/slices/aiChat/actions/streamingStates.ts +0 -34
- package/src/store/chat/slices/aiChat/initialState.ts +0 -28
- package/src/store/chat/slices/aiChat/selectors.test.ts +280 -0
- package/src/store/chat/slices/aiChat/selectors.ts +31 -7
- package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +21 -30
- package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +29 -49
- package/src/store/chat/slices/builtinTool/actions/interpreter.ts +83 -48
- package/src/store/chat/slices/builtinTool/actions/localSystem.ts +78 -28
- package/src/store/chat/slices/builtinTool/actions/search.ts +146 -59
- package/src/store/chat/slices/builtinTool/selectors.test.ts +258 -0
- package/src/store/chat/slices/builtinTool/selectors.ts +25 -4
- package/src/store/chat/slices/message/action.test.ts +134 -16
- package/src/store/chat/slices/message/actions/internals.ts +33 -7
- package/src/store/chat/slices/message/actions/optimisticUpdate.ts +85 -52
- package/src/store/chat/slices/message/initialState.ts +0 -10
- package/src/store/chat/slices/message/selectors/messageState.ts +34 -12
- package/src/store/chat/slices/operation/__tests__/actions.test.ts +712 -16
- package/src/store/chat/slices/operation/__tests__/integration.test.ts +342 -0
- package/src/store/chat/slices/operation/__tests__/selectors.test.ts +257 -17
- package/src/store/chat/slices/operation/actions.ts +218 -11
- package/src/store/chat/slices/operation/selectors.ts +135 -6
- package/src/store/chat/slices/operation/types.ts +29 -3
- package/src/store/chat/slices/plugin/action.test.ts +30 -322
- package/src/store/chat/slices/plugin/actions/internals.ts +0 -14
- package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +21 -19
- package/src/store/chat/slices/plugin/actions/pluginTypes.ts +45 -27
- package/src/store/chat/slices/plugin/actions/publicApi.ts +3 -4
- package/src/store/chat/slices/plugin/actions/workflow.ts +0 -55
- package/src/store/chat/slices/thread/selectors/index.ts +4 -2
- package/src/store/chat/slices/translate/action.ts +54 -41
- package/src/tools/web-browsing/ExecutionRuntime/index.ts +5 -2
- 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
|
-
|
|
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
|
|
16
|
-
|
|
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
|
|
21
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
62
|
-
expect(mockStore.
|
|
63
|
-
expect(mockStore.
|
|
64
|
-
expect(mockStore.
|
|
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(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
439
|
+
{ operationId: expect.any(String) },
|
|
460
440
|
);
|
|
461
441
|
expect(result.current.optimisticUpdatePluginState).toHaveBeenCalledWith(
|
|
462
442
|
messageId,
|
|
463
443
|
expect.any(Object),
|
|
464
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
500
|
+
{ operationId: expect.any(String) },
|
|
521
501
|
);
|
|
522
502
|
});
|
|
523
503
|
});
|