@lobehub/lobehub 2.0.0-next.85 → 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 (88) hide show
  1. package/CHANGELOG.md +25 -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 +9 -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/features/ChatInput/ActionBar/STT/browser.tsx +2 -2
  21. package/src/features/ChatInput/ActionBar/STT/openai.tsx +2 -2
  22. package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -1
  23. package/src/features/Conversation/Messages/User/index.tsx +3 -3
  24. package/src/features/Conversation/Messages/index.tsx +3 -3
  25. package/src/features/Conversation/components/AutoScroll.tsx +2 -2
  26. package/src/services/search.ts +2 -2
  27. package/src/store/chat/agents/GeneralChatAgent.ts +98 -0
  28. package/src/store/chat/agents/__tests__/GeneralChatAgent.test.ts +366 -0
  29. package/src/store/chat/agents/__tests__/createAgentExecutors/call-llm.test.ts +1217 -0
  30. package/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts +1976 -0
  31. package/src/store/chat/agents/__tests__/createAgentExecutors/finish.test.ts +453 -0
  32. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/index.ts +4 -0
  33. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockInstructions.ts +126 -0
  34. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockMessages.ts +94 -0
  35. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockOperations.ts +96 -0
  36. package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockStore.ts +138 -0
  37. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/assertions.ts +185 -0
  38. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/index.ts +3 -0
  39. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/operationTestUtils.ts +94 -0
  40. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/testExecutor.ts +139 -0
  41. package/src/store/chat/agents/__tests__/createAgentExecutors/request-human-approve.test.ts +545 -0
  42. package/src/store/chat/agents/__tests__/createAgentExecutors/resolve-aborted-tools.test.ts +686 -0
  43. package/src/store/chat/agents/createAgentExecutors.ts +313 -80
  44. package/src/store/chat/selectors.ts +1 -0
  45. package/src/store/chat/slices/aiChat/__tests__/ai-chat.integration.test.ts +667 -0
  46. package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +137 -27
  47. package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +163 -125
  48. package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +12 -2
  49. package/src/store/chat/slices/aiChat/actions/__tests__/fixtures.ts +0 -2
  50. package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +0 -2
  51. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +286 -19
  52. package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +0 -112
  53. package/src/store/chat/slices/aiChat/actions/conversationControl.ts +42 -99
  54. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +90 -57
  55. package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +5 -25
  56. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +220 -98
  57. package/src/store/chat/slices/aiChat/actions/streamingStates.ts +0 -34
  58. package/src/store/chat/slices/aiChat/initialState.ts +0 -28
  59. package/src/store/chat/slices/aiChat/selectors.test.ts +280 -0
  60. package/src/store/chat/slices/aiChat/selectors.ts +31 -7
  61. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +21 -30
  62. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +29 -49
  63. package/src/store/chat/slices/builtinTool/actions/interpreter.ts +83 -48
  64. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +78 -28
  65. package/src/store/chat/slices/builtinTool/actions/search.ts +146 -59
  66. package/src/store/chat/slices/builtinTool/selectors.test.ts +258 -0
  67. package/src/store/chat/slices/builtinTool/selectors.ts +25 -4
  68. package/src/store/chat/slices/message/action.test.ts +134 -16
  69. package/src/store/chat/slices/message/actions/internals.ts +33 -7
  70. package/src/store/chat/slices/message/actions/optimisticUpdate.ts +85 -52
  71. package/src/store/chat/slices/message/initialState.ts +0 -10
  72. package/src/store/chat/slices/message/selectors/messageState.ts +34 -12
  73. package/src/store/chat/slices/operation/__tests__/actions.test.ts +712 -16
  74. package/src/store/chat/slices/operation/__tests__/integration.test.ts +342 -0
  75. package/src/store/chat/slices/operation/__tests__/selectors.test.ts +257 -17
  76. package/src/store/chat/slices/operation/actions.ts +218 -11
  77. package/src/store/chat/slices/operation/selectors.ts +135 -6
  78. package/src/store/chat/slices/operation/types.ts +29 -3
  79. package/src/store/chat/slices/plugin/action.test.ts +30 -322
  80. package/src/store/chat/slices/plugin/actions/internals.ts +0 -14
  81. package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +21 -19
  82. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +45 -27
  83. package/src/store/chat/slices/plugin/actions/publicApi.ts +3 -4
  84. package/src/store/chat/slices/plugin/actions/workflow.ts +0 -55
  85. package/src/store/chat/slices/thread/selectors/index.ts +4 -2
  86. package/src/store/chat/slices/translate/action.ts +54 -41
  87. package/src/tools/web-browsing/ExecutionRuntime/index.ts +5 -2
  88. package/src/tools/web-browsing/Portal/Search/Footer.tsx +11 -9
@@ -0,0 +1,258 @@
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 { chatToolSelectors } from './selectors';
7
+
8
+ describe('chatToolSelectors', () => {
9
+ beforeEach(() => {
10
+ useChatStore.setState(useChatStore.getInitialState());
11
+ });
12
+
13
+ describe('isDallEImageGenerating', () => {
14
+ it('should return true when DALL-E image is generating for message', () => {
15
+ const { result } = renderHook(() => useChatStore());
16
+
17
+ act(() => {
18
+ useChatStore.setState({
19
+ dalleImageLoading: {
20
+ msg1: true,
21
+ msg2: false,
22
+ },
23
+ });
24
+ });
25
+
26
+ expect(chatToolSelectors.isDallEImageGenerating('msg1')(result.current)).toBe(true);
27
+ expect(chatToolSelectors.isDallEImageGenerating('msg2')(result.current)).toBe(false);
28
+ });
29
+
30
+ it('should return undefined when message not in loading state', () => {
31
+ const { result } = renderHook(() => useChatStore());
32
+
33
+ expect(chatToolSelectors.isDallEImageGenerating('msg1')(result.current)).toBeUndefined();
34
+ });
35
+ });
36
+
37
+ describe('isGeneratingDallEImage', () => {
38
+ it('should return true when any DALL-E image is generating', () => {
39
+ const { result } = renderHook(() => useChatStore());
40
+
41
+ act(() => {
42
+ useChatStore.setState({
43
+ dalleImageLoading: {
44
+ msg1: false,
45
+ msg2: true,
46
+ msg3: false,
47
+ },
48
+ });
49
+ });
50
+
51
+ expect(chatToolSelectors.isGeneratingDallEImage(result.current)).toBe(true);
52
+ });
53
+
54
+ it('should return false when no DALL-E images are generating', () => {
55
+ const { result } = renderHook(() => useChatStore());
56
+
57
+ act(() => {
58
+ useChatStore.setState({
59
+ dalleImageLoading: {
60
+ msg1: false,
61
+ msg2: false,
62
+ },
63
+ });
64
+ });
65
+
66
+ expect(chatToolSelectors.isGeneratingDallEImage(result.current)).toBe(false);
67
+ });
68
+
69
+ it('should return false when dalleImageLoading is empty', () => {
70
+ const { result } = renderHook(() => useChatStore());
71
+
72
+ expect(chatToolSelectors.isGeneratingDallEImage(result.current)).toBe(false);
73
+ });
74
+ });
75
+
76
+ describe('isInterpreterExecuting', () => {
77
+ it('should return true when interpreter is executing for message', () => {
78
+ const { result } = renderHook(() => useChatStore());
79
+
80
+ let opId: string;
81
+
82
+ act(() => {
83
+ opId = result.current.startOperation({
84
+ type: 'builtinToolInterpreter',
85
+ context: { sessionId: 'session1', messageId: 'msg1' },
86
+ }).operationId;
87
+
88
+ result.current.associateMessageWithOperation('msg1', opId);
89
+ });
90
+
91
+ expect(chatToolSelectors.isInterpreterExecuting('msg1')(result.current)).toBe(true);
92
+ });
93
+
94
+ it('should return false when no operation exists for message', () => {
95
+ const { result } = renderHook(() => useChatStore());
96
+
97
+ expect(chatToolSelectors.isInterpreterExecuting('msg1')(result.current)).toBe(false);
98
+ });
99
+
100
+ it('should return false when operation is not builtinToolInterpreter', () => {
101
+ const { result } = renderHook(() => useChatStore());
102
+
103
+ let opId: string;
104
+
105
+ act(() => {
106
+ opId = result.current.startOperation({
107
+ type: 'execAgentRuntime',
108
+ context: { sessionId: 'session1', messageId: 'msg1' },
109
+ }).operationId;
110
+
111
+ result.current.associateMessageWithOperation('msg1', opId);
112
+ });
113
+
114
+ expect(chatToolSelectors.isInterpreterExecuting('msg1')(result.current)).toBe(false);
115
+ });
116
+
117
+ it('should return false when operation is completed', () => {
118
+ const { result } = renderHook(() => useChatStore());
119
+
120
+ let opId: string;
121
+
122
+ act(() => {
123
+ opId = result.current.startOperation({
124
+ type: 'builtinToolInterpreter',
125
+ context: { sessionId: 'session1', messageId: 'msg1' },
126
+ }).operationId;
127
+
128
+ result.current.associateMessageWithOperation('msg1', opId);
129
+ });
130
+
131
+ act(() => {
132
+ result.current.completeOperation(opId);
133
+ });
134
+
135
+ expect(chatToolSelectors.isInterpreterExecuting('msg1')(result.current)).toBe(false);
136
+ });
137
+ });
138
+
139
+ describe('isSearXNGSearching', () => {
140
+ it('should return true when SearXNG search is running for message', () => {
141
+ const { result } = renderHook(() => useChatStore());
142
+
143
+ let opId: string;
144
+
145
+ act(() => {
146
+ opId = result.current.startOperation({
147
+ type: 'builtinToolSearch',
148
+ context: { sessionId: 'session1', messageId: 'msg1' },
149
+ }).operationId;
150
+
151
+ result.current.associateMessageWithOperation('msg1', opId);
152
+ });
153
+
154
+ expect(chatToolSelectors.isSearXNGSearching('msg1')(result.current)).toBe(true);
155
+ });
156
+
157
+ it('should return false when no operation exists', () => {
158
+ const { result } = renderHook(() => useChatStore());
159
+
160
+ expect(chatToolSelectors.isSearXNGSearching('msg1')(result.current)).toBe(false);
161
+ });
162
+
163
+ it('should return false when operation type is different', () => {
164
+ const { result } = renderHook(() => useChatStore());
165
+
166
+ let opId: string;
167
+
168
+ act(() => {
169
+ opId = result.current.startOperation({
170
+ type: 'builtinToolInterpreter',
171
+ context: { sessionId: 'session1', messageId: 'msg1' },
172
+ }).operationId;
173
+
174
+ result.current.associateMessageWithOperation('msg1', opId);
175
+ });
176
+
177
+ expect(chatToolSelectors.isSearXNGSearching('msg1')(result.current)).toBe(false);
178
+ });
179
+
180
+ it('should return false when operation is not running', () => {
181
+ const { result } = renderHook(() => useChatStore());
182
+
183
+ let opId: string;
184
+
185
+ act(() => {
186
+ opId = result.current.startOperation({
187
+ type: 'builtinToolSearch',
188
+ context: { sessionId: 'session1', messageId: 'msg1' },
189
+ }).operationId;
190
+
191
+ result.current.associateMessageWithOperation('msg1', opId);
192
+ result.current.completeOperation(opId);
193
+ });
194
+
195
+ expect(chatToolSelectors.isSearXNGSearching('msg1')(result.current)).toBe(false);
196
+ });
197
+ });
198
+
199
+ describe('isSearchingLocalFiles', () => {
200
+ it('should return true when local system search is running', () => {
201
+ const { result } = renderHook(() => useChatStore());
202
+
203
+ let opId: string;
204
+
205
+ act(() => {
206
+ opId = result.current.startOperation({
207
+ type: 'builtinToolLocalSystem',
208
+ context: { sessionId: 'session1', messageId: 'msg1' },
209
+ }).operationId;
210
+
211
+ result.current.associateMessageWithOperation('msg1', opId);
212
+ });
213
+
214
+ expect(chatToolSelectors.isSearchingLocalFiles('msg1')(result.current)).toBe(true);
215
+ });
216
+
217
+ it('should return false when no operation exists', () => {
218
+ const { result } = renderHook(() => useChatStore());
219
+
220
+ expect(chatToolSelectors.isSearchingLocalFiles('msg1')(result.current)).toBe(false);
221
+ });
222
+
223
+ it('should return false when operation type is different', () => {
224
+ const { result } = renderHook(() => useChatStore());
225
+
226
+ let opId: string;
227
+
228
+ act(() => {
229
+ opId = result.current.startOperation({
230
+ type: 'builtinToolSearch',
231
+ context: { sessionId: 'session1', messageId: 'msg1' },
232
+ }).operationId;
233
+
234
+ result.current.associateMessageWithOperation('msg1', opId);
235
+ });
236
+
237
+ expect(chatToolSelectors.isSearchingLocalFiles('msg1')(result.current)).toBe(false);
238
+ });
239
+
240
+ it('should return false when operation is completed', () => {
241
+ const { result } = renderHook(() => useChatStore());
242
+
243
+ let opId: string;
244
+
245
+ act(() => {
246
+ opId = result.current.startOperation({
247
+ type: 'builtinToolLocalSystem',
248
+ context: { sessionId: 'session1', messageId: 'msg1' },
249
+ }).operationId;
250
+
251
+ result.current.associateMessageWithOperation('msg1', opId);
252
+ result.current.completeOperation(opId);
253
+ });
254
+
255
+ expect(chatToolSelectors.isSearchingLocalFiles('msg1')(result.current)).toBe(false);
256
+ });
257
+ });
258
+ });
@@ -5,11 +5,32 @@ const isDallEImageGenerating = (id: string) => (s: ChatStoreState) => s.dalleIma
5
5
  const isGeneratingDallEImage = (s: ChatStoreState) =>
6
6
  Object.values(s.dalleImageLoading).some(Boolean);
7
7
 
8
- const isInterpreterExecuting = (id: string) => (s: ChatStoreState) =>
9
- s.codeInterpreterExecuting[id];
8
+ const isInterpreterExecuting = (id: string) => (s: ChatStoreState) => {
9
+ // Check if there's a running builtinToolInterpreter operation for this message
10
+ const operationId = s.messageOperationMap[id];
11
+ if (!operationId) return false;
10
12
 
11
- const isSearXNGSearching = (id: string) => (s: ChatStoreState) => s.searchLoading[id];
12
- const isSearchingLocalFiles = (id: string) => (s: ChatStoreState) => s.localFileLoading[id];
13
+ const operation = s.operations[operationId];
14
+ return operation?.type === 'builtinToolInterpreter' && operation?.status === 'running';
15
+ };
16
+
17
+ const isSearXNGSearching = (id: string) => (s: ChatStoreState) => {
18
+ // Check if there's a running builtinToolSearch operation for this message
19
+ const operationId = s.messageOperationMap[id];
20
+ if (!operationId) return false;
21
+
22
+ const operation = s.operations[operationId];
23
+ return operation?.type === 'builtinToolSearch' && operation?.status === 'running';
24
+ };
25
+
26
+ const isSearchingLocalFiles = (id: string) => (s: ChatStoreState) => {
27
+ // Check if there's a running builtinToolLocalSystem operation for this message
28
+ const operationId = s.messageOperationMap[id];
29
+ if (!operationId) return false;
30
+
31
+ const operation = s.operations[operationId];
32
+ return operation?.type === 'builtinToolLocalSystem' && operation?.status === 'running';
33
+ };
13
34
 
14
35
  export const chatToolSelectors = {
15
36
  isDallEImageGenerating,
@@ -28,6 +28,7 @@ vi.mock('@/services/message', () => ({
28
28
  createMessage: vi.fn(() => Promise.resolve({ id: 'new-message-id', messages: [] })),
29
29
  updateMessage: vi.fn(() => Promise.resolve({ success: true, messages: [] })),
30
30
  updateMessageMetadata: vi.fn(() => Promise.resolve({ success: true, messages: [] })),
31
+ updateMessagePlugin: vi.fn(() => Promise.resolve({ success: true, messages: [] })),
31
32
  updateMessagePluginError: vi.fn(() => Promise.resolve({ success: true, messages: [] })),
32
33
  updateMessageRAG: vi.fn(() => Promise.resolve({ success: true, messages: [] })),
33
34
  removeAllMessages: vi.fn(() => Promise.resolve()),
@@ -688,11 +689,14 @@ describe('chatMessage actions', () => {
688
689
  await result.current.optimisticUpdateMessageContent(messageId, newContent);
689
690
  });
690
691
 
691
- expect(internal_dispatchMessageSpy).toHaveBeenCalledWith({
692
- id: messageId,
693
- type: 'updateMessage',
694
- value: { content: newContent },
695
- });
692
+ expect(internal_dispatchMessageSpy).toHaveBeenCalledWith(
693
+ {
694
+ id: messageId,
695
+ type: 'updateMessage',
696
+ value: { content: newContent },
697
+ },
698
+ undefined,
699
+ );
696
700
  });
697
701
 
698
702
  it('should replace messages after updating content', async () => {
@@ -872,10 +876,17 @@ describe('chatMessage actions', () => {
872
876
 
873
877
  const updateMessageSpy = vi.spyOn(messageService, 'updateMessage');
874
878
 
879
+ let operationId: string;
875
880
  await act(async () => {
881
+ // Create operation with desired context
882
+ const op = result.current.startOperation({
883
+ type: 'sendMessage',
884
+ context: { sessionId: contextSessionId, topicId: contextTopicId },
885
+ });
886
+ operationId = op.operationId;
887
+
876
888
  await result.current.optimisticUpdateMessageContent(messageId, content, undefined, {
877
- sessionId: contextSessionId,
878
- topicId: contextTopicId,
889
+ operationId,
879
890
  });
880
891
  });
881
892
 
@@ -886,7 +897,7 @@ describe('chatMessage actions', () => {
886
897
  );
887
898
  });
888
899
 
889
- it('optimisticUpdateMessageError should use context sessionId/topicId', async () => {
900
+ it('optimisticUpdateMessageError should use context operationId', async () => {
890
901
  const { result } = renderHook(() => useChatStore());
891
902
  const messageId = 'message-id';
892
903
  const error = { message: 'Error occurred', type: 'error' as any };
@@ -895,10 +906,17 @@ describe('chatMessage actions', () => {
895
906
 
896
907
  const updateMessageSpy = vi.spyOn(messageService, 'updateMessage');
897
908
 
909
+ let operationId: string;
898
910
  await act(async () => {
911
+ // Create operation with desired context
912
+ const op = result.current.startOperation({
913
+ type: 'sendMessage',
914
+ context: { sessionId: contextSessionId, topicId: contextTopicId },
915
+ });
916
+ operationId = op.operationId;
917
+
899
918
  await result.current.optimisticUpdateMessageError(messageId, error, {
900
- sessionId: contextSessionId,
901
- topicId: contextTopicId,
919
+ operationId,
902
920
  });
903
921
  });
904
922
 
@@ -909,7 +927,7 @@ describe('chatMessage actions', () => {
909
927
  );
910
928
  });
911
929
 
912
- it('optimisticDeleteMessage should use context sessionId/topicId', async () => {
930
+ it('optimisticDeleteMessage should use context operationId', async () => {
913
931
  const { result } = renderHook(() => useChatStore());
914
932
  const messageId = 'message-id';
915
933
  const contextSessionId = 'context-session';
@@ -917,10 +935,17 @@ describe('chatMessage actions', () => {
917
935
 
918
936
  const removeMessageSpy = vi.spyOn(messageService, 'removeMessage');
919
937
 
938
+ let operationId: string;
920
939
  await act(async () => {
940
+ // Create operation with desired context
941
+ const op = result.current.startOperation({
942
+ type: 'sendMessage',
943
+ context: { sessionId: contextSessionId, topicId: contextTopicId },
944
+ });
945
+ operationId = op.operationId;
946
+
921
947
  await result.current.optimisticDeleteMessage(messageId, {
922
- sessionId: contextSessionId,
923
- topicId: contextTopicId,
948
+ operationId,
924
949
  });
925
950
  });
926
951
 
@@ -930,7 +955,7 @@ describe('chatMessage actions', () => {
930
955
  });
931
956
  });
932
957
 
933
- it('optimisticDeleteMessages should use context sessionId/topicId', async () => {
958
+ it('optimisticDeleteMessages should use context operationId', async () => {
934
959
  const { result } = renderHook(() => useChatStore());
935
960
  const ids = ['id-1', 'id-2'];
936
961
  const contextSessionId = 'context-session';
@@ -938,10 +963,17 @@ describe('chatMessage actions', () => {
938
963
 
939
964
  const removeMessagesSpy = vi.spyOn(messageService, 'removeMessages');
940
965
 
966
+ let operationId: string;
941
967
  await act(async () => {
968
+ // Create operation with desired context
969
+ const op = result.current.startOperation({
970
+ type: 'sendMessage',
971
+ context: { sessionId: contextSessionId, topicId: contextTopicId },
972
+ });
973
+ operationId = op.operationId;
974
+
942
975
  await result.current.optimisticDeleteMessages(ids, {
943
- sessionId: contextSessionId,
944
- topicId: contextTopicId,
976
+ operationId,
945
977
  });
946
978
  });
947
979
 
@@ -951,4 +983,90 @@ describe('chatMessage actions', () => {
951
983
  });
952
984
  });
953
985
  });
986
+
987
+ describe('optimisticUpdateMessagePlugin', () => {
988
+ it('should dispatch message update action with plugin value', async () => {
989
+ const { result } = renderHook(() => useChatStore());
990
+ const messageId = 'message-id';
991
+ const pluginValue = { arguments: '{"test":"value"}' };
992
+ const internal_dispatchMessageSpy = vi.spyOn(result.current, 'internal_dispatchMessage');
993
+
994
+ await act(async () => {
995
+ await result.current.optimisticUpdateMessagePlugin(messageId, pluginValue);
996
+ });
997
+
998
+ expect(internal_dispatchMessageSpy).toHaveBeenCalledWith(
999
+ {
1000
+ id: messageId,
1001
+ type: 'updateMessagePlugin',
1002
+ value: pluginValue,
1003
+ },
1004
+ undefined,
1005
+ );
1006
+ });
1007
+
1008
+ it('should call messageService.updateMessagePlugin with correct parameters', async () => {
1009
+ const { result } = renderHook(() => useChatStore());
1010
+ const messageId = 'message-id';
1011
+ const pluginValue = { state: 'success' };
1012
+
1013
+ const updateMessagePluginSpy = vi.spyOn(messageService, 'updateMessagePlugin');
1014
+ await act(async () => {
1015
+ await result.current.optimisticUpdateMessagePlugin(messageId, pluginValue);
1016
+ });
1017
+
1018
+ expect(updateMessagePluginSpy).toHaveBeenCalledWith(messageId, pluginValue, {
1019
+ sessionId: 'session-id',
1020
+ topicId: 'topic-id',
1021
+ });
1022
+ });
1023
+
1024
+ it('should replace messages after updating plugin', async () => {
1025
+ const { result } = renderHook(() => useChatStore());
1026
+ const messageId = 'message-id';
1027
+ const pluginValue = { apiName: 'test-api' };
1028
+ const replaceMessagesSpy = vi.spyOn(result.current, 'replaceMessages');
1029
+
1030
+ await act(async () => {
1031
+ await result.current.optimisticUpdateMessagePlugin(messageId, pluginValue);
1032
+ });
1033
+
1034
+ expect(replaceMessagesSpy).toHaveBeenCalledWith(
1035
+ [],
1036
+ expect.objectContaining({
1037
+ sessionId: 'session-id',
1038
+ topicId: 'topic-id',
1039
+ }),
1040
+ );
1041
+ });
1042
+
1043
+ it('should use context operationId when provided', async () => {
1044
+ const { result } = renderHook(() => useChatStore());
1045
+ const messageId = 'message-id';
1046
+ const pluginValue = { identifier: 'test-plugin' };
1047
+ const contextSessionId = 'context-session';
1048
+ const contextTopicId = 'context-topic';
1049
+
1050
+ const updateMessagePluginSpy = vi.spyOn(messageService, 'updateMessagePlugin');
1051
+
1052
+ let operationId: string;
1053
+ await act(async () => {
1054
+ // Create operation with desired context
1055
+ const op = result.current.startOperation({
1056
+ type: 'sendMessage',
1057
+ context: { sessionId: contextSessionId, topicId: contextTopicId },
1058
+ });
1059
+ operationId = op.operationId;
1060
+
1061
+ await result.current.optimisticUpdateMessagePlugin(messageId, pluginValue, {
1062
+ operationId,
1063
+ });
1064
+ });
1065
+
1066
+ expect(updateMessagePluginSpy).toHaveBeenCalledWith(messageId, pluginValue, {
1067
+ sessionId: contextSessionId,
1068
+ topicId: contextTopicId,
1069
+ });
1070
+ });
1071
+ });
954
1072
  });
@@ -1,5 +1,6 @@
1
1
  import { parse } from '@lobechat/conversation-flow';
2
2
  import { TraceEventPayloads } from '@lobechat/types';
3
+ import debug from 'debug';
3
4
  import isEqual from 'fast-deep-equal';
4
5
  import { StateCreator } from 'zustand/vanilla';
5
6
 
@@ -10,6 +11,8 @@ import { displayMessageSelectors } from '../../../selectors';
10
11
  import { messageMapKey } from '../../../utils/messageMapKey';
11
12
  import { MessageDispatch, messagesReducer } from '../reducer';
12
13
 
14
+ const log = debug('lobe-store:message-internals');
15
+
13
16
  /**
14
17
  * Internal core methods that serve as building blocks for other actions
15
18
  */
@@ -18,10 +21,7 @@ export interface MessageInternalsAction {
18
21
  * update message at the frontend
19
22
  * this method will not update messages to database
20
23
  */
21
- internal_dispatchMessage: (
22
- payload: MessageDispatch,
23
- context?: { sessionId: string; topicId?: string | null },
24
- ) => void;
24
+ internal_dispatchMessage: (payload: MessageDispatch, context?: { operationId?: string }) => void;
25
25
 
26
26
  /**
27
27
  * trace message events for analytics
@@ -37,10 +37,36 @@ export const messageInternals: StateCreator<
37
37
  > = (set, get) => ({
38
38
  // the internal process method of the AI message
39
39
  internal_dispatchMessage: (payload, context) => {
40
- const activeId = typeof context !== 'undefined' ? context.sessionId : get().activeId;
41
- const topicId = typeof context !== 'undefined' ? context.topicId : get().activeTopicId;
40
+ let sessionId: string;
41
+ let topicId: string | null | undefined;
42
+
43
+ // Get context from operation if operationId is provided
44
+ if (context?.operationId) {
45
+ const operation = get().operations[context.operationId];
46
+ if (!operation) {
47
+ log('[internal_dispatchMessage] ERROR: Operation not found: %s', context.operationId);
48
+ throw new Error(`Operation not found: ${context.operationId}`);
49
+ }
50
+ sessionId = operation.context.sessionId!;
51
+ topicId = operation.context.topicId;
52
+ log(
53
+ '[internal_dispatchMessage] get context from operation %s: sessionId=%s, topicId=%s',
54
+ context.operationId,
55
+ sessionId,
56
+ topicId,
57
+ );
58
+ } else {
59
+ // Fallback to global state
60
+ sessionId = get().activeId;
61
+ topicId = get().activeTopicId;
62
+ log(
63
+ '[internal_dispatchMessage] use global context: sessionId=%s, topicId=%s',
64
+ sessionId,
65
+ topicId,
66
+ );
67
+ }
42
68
 
43
- const messagesKey = messageMapKey(activeId, topicId);
69
+ const messagesKey = messageMapKey(sessionId, topicId);
44
70
 
45
71
  // Get raw messages from dbMessagesMap and apply reducer
46
72
  const rawMessages = get().dbMessagesMap[messagesKey] || [];