@lobehub/lobehub 2.0.0-next.104 → 2.0.0-next.106

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 (86) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/apps/desktop/package.json +2 -2
  3. package/changelog/v1.json +14 -0
  4. package/locales/ar/image.json +8 -0
  5. package/locales/ar/models.json +110 -64
  6. package/locales/ar/providers.json +3 -0
  7. package/locales/bg-BG/image.json +8 -0
  8. package/locales/bg-BG/models.json +98 -68
  9. package/locales/bg-BG/providers.json +3 -0
  10. package/locales/de-DE/image.json +8 -0
  11. package/locales/de-DE/models.json +176 -38
  12. package/locales/de-DE/providers.json +3 -0
  13. package/locales/en-US/image.json +8 -0
  14. package/locales/en-US/models.json +176 -38
  15. package/locales/en-US/providers.json +3 -0
  16. package/locales/es-ES/image.json +8 -0
  17. package/locales/es-ES/models.json +176 -38
  18. package/locales/es-ES/providers.json +3 -0
  19. package/locales/fa-IR/image.json +8 -0
  20. package/locales/fa-IR/models.json +110 -64
  21. package/locales/fa-IR/providers.json +3 -0
  22. package/locales/fr-FR/image.json +8 -0
  23. package/locales/fr-FR/models.json +110 -64
  24. package/locales/fr-FR/providers.json +3 -0
  25. package/locales/it-IT/image.json +8 -0
  26. package/locales/it-IT/models.json +176 -38
  27. package/locales/it-IT/providers.json +3 -0
  28. package/locales/ja-JP/image.json +8 -0
  29. package/locales/ja-JP/models.json +110 -64
  30. package/locales/ja-JP/providers.json +3 -0
  31. package/locales/ko-KR/image.json +8 -0
  32. package/locales/ko-KR/models.json +110 -64
  33. package/locales/ko-KR/providers.json +3 -0
  34. package/locales/nl-NL/image.json +8 -0
  35. package/locales/nl-NL/models.json +176 -38
  36. package/locales/nl-NL/providers.json +3 -0
  37. package/locales/pl-PL/image.json +8 -0
  38. package/locales/pl-PL/models.json +110 -64
  39. package/locales/pl-PL/providers.json +3 -0
  40. package/locales/pt-BR/image.json +8 -0
  41. package/locales/pt-BR/models.json +176 -38
  42. package/locales/pt-BR/providers.json +3 -0
  43. package/locales/ru-RU/image.json +8 -0
  44. package/locales/ru-RU/models.json +98 -68
  45. package/locales/ru-RU/providers.json +3 -0
  46. package/locales/tr-TR/image.json +8 -0
  47. package/locales/tr-TR/models.json +110 -64
  48. package/locales/tr-TR/providers.json +3 -0
  49. package/locales/vi-VN/image.json +8 -0
  50. package/locales/vi-VN/models.json +176 -38
  51. package/locales/vi-VN/providers.json +3 -0
  52. package/locales/zh-CN/image.json +8 -0
  53. package/locales/zh-CN/models.json +179 -38
  54. package/locales/zh-CN/providers.json +3 -0
  55. package/locales/zh-TW/image.json +8 -0
  56. package/locales/zh-TW/models.json +176 -38
  57. package/locales/zh-TW/providers.json +3 -0
  58. package/package.json +9 -3
  59. package/packages/database/src/repositories/knowledge/index.ts +5 -8
  60. package/packages/model-bank/src/aiModels/moonshot.ts +46 -0
  61. package/packages/model-runtime/src/core/contextBuilders/openai.ts +1 -1
  62. package/packages/model-runtime/src/providers/moonshot/index.ts +17 -4
  63. package/packages/model-runtime/src/utils/postProcessModelList.ts +15 -13
  64. package/packages/types/src/user/settings/keyVaults.ts +0 -68
  65. package/packages/utils/src/client/parserPlaceholder.ts +1 -1
  66. package/src/services/__tests__/_auth.test.ts +1 -4
  67. package/src/services/_auth.ts +2 -3
  68. package/src/services/_header.ts +1 -8
  69. package/src/store/chat/agents/__tests__/createAgentExecutors/call-llm.test.ts +18 -0
  70. package/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts +40 -11
  71. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/assertions.ts +3 -0
  72. package/src/store/chat/agents/__tests__/createAgentExecutors/request-human-approve.test.ts +15 -0
  73. package/src/store/chat/agents/__tests__/createAgentExecutors/resolve-aborted-tools.test.ts +37 -11
  74. package/src/store/chat/agents/createAgentExecutors.ts +22 -13
  75. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +4 -8
  76. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +16 -2
  77. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +5 -1
  78. package/src/store/chat/slices/builtinTool/actions/search.ts +5 -1
  79. package/src/store/chat/slices/message/actions/publicApi.ts +10 -2
  80. package/src/store/chat/slices/message/actions/query.ts +17 -4
  81. package/src/store/chat/slices/operation/__tests__/selectors.test.ts +93 -5
  82. package/src/store/chat/slices/operation/selectors.ts +16 -3
  83. package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +24 -18
  84. package/src/store/user/slices/settings/selectors/keyVaults.ts +0 -5
  85. package/src/features/ChatList/Error/AccessCodeForm.tsx +0 -63
  86. package/src/services/__tests__/share.test.ts +0 -61
@@ -18,12 +18,25 @@ export const params = {
18
18
  handlePayload: (payload: ChatStreamPayload) => {
19
19
  const { enabledSearch, messages, temperature, tools, ...rest } = payload;
20
20
 
21
- // assistant 空消息添加一个空格 (#8418)
22
- const filteredMessages = messages.map((message) => {
21
+ const filteredMessages = messages.map((message: any) => {
22
+ let normalizedMessage = message;
23
+
24
+ // 为 assistant 空消息添加一个空格 (#8418)
23
25
  if (message.role === 'assistant' && (!message.content || message.content === '')) {
24
- return { ...message, content: ' ' };
26
+ normalizedMessage = { ...normalizedMessage, content: ' ' };
27
+ }
28
+
29
+ // Interleaved thinking
30
+ if (message.role === 'assistant' && message.reasoning) {
31
+ const { reasoning, ...messageWithoutReasoning } = normalizedMessage;
32
+ return {
33
+ ...messageWithoutReasoning,
34
+ ...(!reasoning.signature && reasoning.content
35
+ ? { reasoning_content: reasoning.content }
36
+ : {}),
37
+ };
25
38
  }
26
- return message;
39
+ return normalizedMessage;
27
40
  });
28
41
 
29
42
  const moonshotTools = enabledSearch
@@ -1,10 +1,13 @@
1
1
  import type { ChatModelCard } from '@lobechat/types';
2
+ import { omit } from 'lodash-es';
2
3
  import { AiModelType, CHAT_MODEL_IMAGE_GENERATION_PARAMS } from 'model-bank';
3
4
 
4
5
  // Whitelist for automatic image model generation
5
6
  export const IMAGE_GENERATION_MODEL_WHITELIST = [
6
7
  'gemini-2.5-flash-image-preview',
7
8
  'gemini-2.5-flash-image-preview:free',
9
+ 'gemini-3-pro-image-preview',
10
+ 'gemini-3-pro-image-preview:free',
8
11
  // More models can be added in the future
9
12
  ] as const;
10
13
 
@@ -41,19 +44,18 @@ export async function postProcessModelList(
41
44
  const matchingModels = finalModels.filter((model) => model.id.endsWith(whitelistPattern));
42
45
 
43
46
  for (const model of matchingModels) {
44
- // Blacklist: remove unnecessary properties, keep the rest
45
- const {
46
- files, // drop
47
- functionCall, // drop
48
- reasoning, // drop
49
- search, // drop
50
- imageOutput, // drop
51
- video, // drop
52
- vision, // drop
53
- type: _dropType, // will be overwritten
54
- parameters: _dropParams, // will be overwritten
55
- ...rest
56
- } = model;
47
+ // Remove unnecessary properties, keep the rest
48
+ const rest = omit(model, [
49
+ 'files',
50
+ 'functionCall',
51
+ 'reasoning',
52
+ 'search',
53
+ 'imageOutput',
54
+ 'video',
55
+ 'vision',
56
+ 'type',
57
+ 'parameters',
58
+ ]);
57
59
 
58
60
  imageModels.push({
59
61
  ...rest, // Keep other fields (such as displayName, pricing, enabled, contextWindowTokens, etc.)
@@ -51,73 +51,5 @@ export interface SearchEngineKeyVaults {
51
51
  }
52
52
 
53
53
  export interface UserKeyVaults extends SearchEngineKeyVaults {
54
- ai21?: OpenAICompatibleKeyVault;
55
- ai302?: OpenAICompatibleKeyVault;
56
- ai360?: OpenAICompatibleKeyVault;
57
- aihubmix?: OpenAICompatibleKeyVault;
58
- akashchat?: OpenAICompatibleKeyVault;
59
- anthropic?: OpenAICompatibleKeyVault;
60
- azure?: AzureOpenAIKeyVault;
61
- azureai?: AzureOpenAIKeyVault;
62
- baichuan?: OpenAICompatibleKeyVault;
63
- bedrock?: AWSBedrockKeyVault;
64
- bfl?: any;
65
- cerebras?: OpenAICompatibleKeyVault;
66
- cloudflare?: CloudflareKeyVault;
67
- cohere?: OpenAICompatibleKeyVault;
68
- cometapi?: OpenAICompatibleKeyVault;
69
- comfyui?: ComfyUIKeyVault;
70
- deepseek?: OpenAICompatibleKeyVault;
71
- fal?: FalKeyVault;
72
- fireworksai?: OpenAICompatibleKeyVault;
73
- giteeai?: OpenAICompatibleKeyVault;
74
- github?: OpenAICompatibleKeyVault;
75
- google?: OpenAICompatibleKeyVault;
76
- groq?: OpenAICompatibleKeyVault;
77
- higress?: OpenAICompatibleKeyVault;
78
- huggingface?: OpenAICompatibleKeyVault;
79
- hunyuan?: OpenAICompatibleKeyVault;
80
- infiniai?: OpenAICompatibleKeyVault;
81
- internlm?: OpenAICompatibleKeyVault;
82
- jina?: OpenAICompatibleKeyVault;
83
- lmstudio?: OpenAICompatibleKeyVault;
84
- lobehub?: any;
85
- minimax?: OpenAICompatibleKeyVault;
86
- mistral?: OpenAICompatibleKeyVault;
87
- modelscope?: OpenAICompatibleKeyVault;
88
- moonshot?: OpenAICompatibleKeyVault;
89
- nebius?: OpenAICompatibleKeyVault;
90
- newapi?: OpenAICompatibleKeyVault;
91
- novita?: OpenAICompatibleKeyVault;
92
- nvidia?: OpenAICompatibleKeyVault;
93
- ollama?: OpenAICompatibleKeyVault;
94
- ollamacloud?: OpenAICompatibleKeyVault;
95
- openai?: OpenAICompatibleKeyVault;
96
- openrouter?: OpenAICompatibleKeyVault;
97
- password?: string;
98
- perplexity?: OpenAICompatibleKeyVault;
99
- ppio?: OpenAICompatibleKeyVault;
100
- qiniu?: OpenAICompatibleKeyVault;
101
- qwen?: OpenAICompatibleKeyVault;
102
- sambanova?: OpenAICompatibleKeyVault;
103
54
  search1api?: OpenAICompatibleKeyVault;
104
- sensenova?: OpenAICompatibleKeyVault;
105
- siliconcloud?: OpenAICompatibleKeyVault;
106
- spark?: OpenAICompatibleKeyVault;
107
- stepfun?: OpenAICompatibleKeyVault;
108
- taichu?: OpenAICompatibleKeyVault;
109
- tencentcloud?: OpenAICompatibleKeyVault;
110
- togetherai?: OpenAICompatibleKeyVault;
111
- upstage?: OpenAICompatibleKeyVault;
112
- v0?: OpenAICompatibleKeyVault;
113
- vercelaigateway?: OpenAICompatibleKeyVault;
114
- vertexai?: VertexAIKeyVault;
115
- vllm?: OpenAICompatibleKeyVault;
116
- volcengine?: OpenAICompatibleKeyVault;
117
- wenxin?: OpenAICompatibleKeyVault;
118
- xai?: OpenAICompatibleKeyVault;
119
- xinference?: OpenAICompatibleKeyVault;
120
- zenmux?: OpenAICompatibleKeyVault;
121
- zeroone?: OpenAICompatibleKeyVault;
122
- zhipu?: OpenAICompatibleKeyVault;
123
55
  }
@@ -87,7 +87,7 @@ export const VARIABLE_GENERATORS = {
87
87
  random_digit: () => Math.floor(Math.random() * 10).toString(),
88
88
 
89
89
  /**
90
- * UUID 类模板变量
90
+ * UUID-type template variables
91
91
  *
92
92
  * | Value | Example |
93
93
  * |-------|---------|
@@ -23,10 +23,7 @@ const mockTogetherAIAPIKey = 'togetherai-api-key';
23
23
  // mock the traditional zustand
24
24
  vi.mock('zustand/traditional');
25
25
 
26
- const setModelProviderConfig = <T extends GlobalLLMProviderKey>(
27
- provider: T,
28
- config: Partial<UserKeyVaults[T]>,
29
- ) => {
26
+ const setModelProviderConfig = (provider: string, config: any) => {
30
27
  useUserStore.setState({
31
28
  settings: { keyVaults: { [provider]: config } },
32
29
  });
@@ -13,7 +13,7 @@ import { ModelProvider } from 'model-bank';
13
13
 
14
14
  import { aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra';
15
15
  import { useUserStore } from '@/store/user';
16
- import { keyVaultsConfigSelectors, userProfileSelectors } from '@/store/user/selectors';
16
+ import { userProfileSelectors } from '@/store/user/selectors';
17
17
  import { obfuscatePayloadWithXOR } from '@/utils/client/xor-obfuscation';
18
18
 
19
19
  import { resolveRuntimeProvider } from './chat/helper';
@@ -105,10 +105,9 @@ export const getProviderAuthPayload = (
105
105
  };
106
106
 
107
107
  const createAuthTokenWithPayload = (payload = {}) => {
108
- const accessCode = keyVaultsConfigSelectors.password(useUserStore.getState());
109
108
  const userId = userProfileSelectors.userId(useUserStore.getState());
110
109
 
111
- return obfuscatePayloadWithXOR<ClientSecretPayload>({ accessCode, userId, ...payload });
110
+ return obfuscatePayloadWithXOR<ClientSecretPayload>({ userId, ...payload });
112
111
  };
113
112
 
114
113
  interface AuthParams {
@@ -1,12 +1,6 @@
1
- import {
2
- LOBE_CHAT_ACCESS_CODE,
3
- LOBE_USER_ID,
4
- OPENAI_API_KEY_HEADER_KEY,
5
- OPENAI_END_POINT,
6
- } from '@/const/fetch';
1
+ import { LOBE_USER_ID, OPENAI_API_KEY_HEADER_KEY, OPENAI_END_POINT } from '@/const/fetch';
7
2
  import { aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra';
8
3
  import { useUserStore } from '@/store/user';
9
- import { keyVaultsConfigSelectors } from '@/store/user/selectors';
10
4
 
11
5
  /**
12
6
  * TODO: Need to be removed after tts refactor
@@ -22,7 +16,6 @@ export const createHeaderWithOpenAI = (header?: HeadersInit): HeadersInit => {
22
16
  // eslint-disable-next-line no-undef
23
17
  return {
24
18
  ...header,
25
- [LOBE_CHAT_ACCESS_CODE]: keyVaultsConfigSelectors.password(state),
26
19
  [LOBE_USER_ID]: state.user?.id || '',
27
20
  [OPENAI_API_KEY_HEADER_KEY]: keyVaults.apiKey || '',
28
21
  [OPENAI_END_POINT]: keyVaults.baseURL || '',
@@ -58,6 +58,9 @@ describe('call_llm executor', () => {
58
58
  sessionId: 'test-session',
59
59
  topicId: 'test-topic',
60
60
  }),
61
+ expect.objectContaining({
62
+ operationId: expect.any(String),
63
+ }),
61
64
  );
62
65
  });
63
66
 
@@ -229,6 +232,9 @@ describe('call_llm executor', () => {
229
232
  expect.objectContaining({
230
233
  parentId: 'msg_payload_parent',
231
234
  }),
235
+ expect.objectContaining({
236
+ operationId: expect.any(String),
237
+ }),
232
238
  );
233
239
  });
234
240
 
@@ -262,6 +268,9 @@ describe('call_llm executor', () => {
262
268
  expect.objectContaining({
263
269
  parentId: 'msg_context_parent',
264
270
  }),
271
+ expect.objectContaining({
272
+ operationId: expect.any(String),
273
+ }),
265
274
  );
266
275
  });
267
276
  });
@@ -1061,6 +1070,9 @@ describe('call_llm executor', () => {
1061
1070
  model: 'claude-3-opus',
1062
1071
  provider: 'anthropic',
1063
1072
  }),
1073
+ expect.objectContaining({
1074
+ operationId: expect.any(String),
1075
+ }),
1064
1076
  );
1065
1077
  expect(mockStore.internal_fetchAIChatMessage).toHaveBeenCalledWith(
1066
1078
  expect.objectContaining({
@@ -1180,6 +1192,9 @@ describe('call_llm executor', () => {
1180
1192
  expect.objectContaining({
1181
1193
  threadId,
1182
1194
  }),
1195
+ expect.objectContaining({
1196
+ operationId: expect.any(String),
1197
+ }),
1183
1198
  );
1184
1199
  });
1185
1200
 
@@ -1211,6 +1226,9 @@ describe('call_llm executor', () => {
1211
1226
  expect.objectContaining({
1212
1227
  threadId: undefined,
1213
1228
  }),
1229
+ expect.objectContaining({
1230
+ operationId: expect.any(String),
1231
+ }),
1214
1232
  );
1215
1233
  });
1216
1234
  });
@@ -156,17 +156,22 @@ describe('call_tool executor', () => {
156
156
  });
157
157
 
158
158
  // Then
159
- expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith({
160
- content: '',
161
- groupId: 'group_789',
162
- parentId: 'msg_parent_123',
163
- plugin: toolCall,
164
- role: 'tool',
165
- sessionId: 'sess_123',
166
- threadId: undefined,
167
- tool_call_id: 'tool_call_xyz',
168
- topicId: 'topic_456',
169
- });
159
+ expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith(
160
+ expect.objectContaining({
161
+ content: '',
162
+ groupId: 'group_789',
163
+ parentId: 'msg_parent_123',
164
+ plugin: toolCall,
165
+ role: 'tool',
166
+ sessionId: 'sess_123',
167
+ threadId: undefined,
168
+ tool_call_id: 'tool_call_xyz',
169
+ topicId: 'topic_456',
170
+ }),
171
+ expect.objectContaining({
172
+ operationId: expect.any(String),
173
+ }),
174
+ );
170
175
  });
171
176
 
172
177
  it('should use assistant message groupId for tool message', async () => {
@@ -194,6 +199,9 @@ describe('call_tool executor', () => {
194
199
  expect.objectContaining({
195
200
  groupId: 'group_special',
196
201
  }),
202
+ expect.objectContaining({
203
+ operationId: expect.any(String),
204
+ }),
197
205
  );
198
206
  });
199
207
 
@@ -222,6 +230,9 @@ describe('call_tool executor', () => {
222
230
  expect.objectContaining({
223
231
  parentId: 'msg_custom_parent',
224
232
  }),
233
+ expect.objectContaining({
234
+ operationId: expect.any(String),
235
+ }),
225
236
  );
226
237
  });
227
238
 
@@ -262,6 +273,9 @@ describe('call_tool executor', () => {
262
273
  expect.objectContaining({
263
274
  plugin: toolCall,
264
275
  }),
276
+ expect.objectContaining({
277
+ operationId: expect.any(String),
278
+ }),
265
279
  );
266
280
  });
267
281
  });
@@ -1542,6 +1556,9 @@ describe('call_tool executor', () => {
1542
1556
  expect.objectContaining({
1543
1557
  groupId: undefined,
1544
1558
  }),
1559
+ expect.objectContaining({
1560
+ operationId: expect.any(String),
1561
+ }),
1545
1562
  );
1546
1563
  expect(result.events).toHaveLength(1);
1547
1564
  });
@@ -1570,6 +1587,9 @@ describe('call_tool executor', () => {
1570
1587
  expect.objectContaining({
1571
1588
  groupId: undefined,
1572
1589
  }),
1590
+ expect.objectContaining({
1591
+ operationId: expect.any(String),
1592
+ }),
1573
1593
  );
1574
1594
  });
1575
1595
 
@@ -1641,6 +1661,9 @@ describe('call_tool executor', () => {
1641
1661
  expect.objectContaining({
1642
1662
  topicId: undefined,
1643
1663
  }),
1664
+ expect.objectContaining({
1665
+ operationId: expect.any(String),
1666
+ }),
1644
1667
  );
1645
1668
  });
1646
1669
 
@@ -1679,6 +1702,9 @@ describe('call_tool executor', () => {
1679
1702
  type: 'builtin',
1680
1703
  }),
1681
1704
  }),
1705
+ expect.objectContaining({
1706
+ operationId: expect.any(String),
1707
+ }),
1682
1708
  );
1683
1709
  expect(result.events).toHaveLength(1);
1684
1710
  });
@@ -1768,6 +1794,9 @@ describe('call_tool executor', () => {
1768
1794
  expect.objectContaining({
1769
1795
  groupId: 'group_latest',
1770
1796
  }),
1797
+ expect.objectContaining({
1798
+ operationId: expect.any(String),
1799
+ }),
1771
1800
  );
1772
1801
  });
1773
1802
  });
@@ -22,6 +22,9 @@ export const expectMessageCreated = (mockStore: ChatStore, role: 'assistant' | '
22
22
  expect.objectContaining({
23
23
  role,
24
24
  }),
25
+ expect.objectContaining({
26
+ operationId: expect.any(String),
27
+ }),
25
28
  );
26
29
  };
27
30
 
@@ -51,6 +51,9 @@ describe('request_human_approve executor', () => {
51
51
  parentId: 'msg_assistant',
52
52
  groupId: assistantMessage.groupId,
53
53
  }),
54
+ expect.objectContaining({
55
+ operationId: expect.any(String),
56
+ }),
54
57
  );
55
58
  });
56
59
 
@@ -205,6 +208,9 @@ describe('request_human_approve executor', () => {
205
208
  expect.objectContaining({
206
209
  groupId: 'group_123',
207
210
  }),
211
+ expect.objectContaining({
212
+ operationId: expect.any(String),
213
+ }),
208
214
  );
209
215
  });
210
216
 
@@ -232,6 +238,9 @@ describe('request_human_approve executor', () => {
232
238
  expect.objectContaining({
233
239
  parentId: 'msg_assistant_456',
234
240
  }),
241
+ expect.objectContaining({
242
+ operationId: expect.any(String),
243
+ }),
235
244
  );
236
245
  });
237
246
  });
@@ -330,6 +339,9 @@ describe('request_human_approve executor', () => {
330
339
  tool_call_id: toolCall.id,
331
340
  pluginIntervention: { status: 'pending' },
332
341
  }),
342
+ expect.objectContaining({
343
+ operationId: expect.any(String),
344
+ }),
333
345
  );
334
346
  });
335
347
  });
@@ -539,6 +551,9 @@ describe('request_human_approve executor', () => {
539
551
  expect.objectContaining({
540
552
  parentId: 'msg_3_last',
541
553
  }),
554
+ expect.objectContaining({
555
+ operationId: expect.any(String),
556
+ }),
542
557
  );
543
558
  });
544
559
  });
@@ -57,6 +57,9 @@ describe('resolve_aborted_tools executor', () => {
57
57
  topicId: 'test-topic',
58
58
  parentId: parentMessage.id,
59
59
  }),
60
+ expect.objectContaining({
61
+ operationId: expect.any(String),
62
+ }),
60
63
  );
61
64
  });
62
65
 
@@ -114,6 +117,9 @@ describe('resolve_aborted_tools executor', () => {
114
117
  pluginIntervention: { status: 'aborted' },
115
118
  tool_call_id: toolCall.id,
116
119
  }),
120
+ expect.objectContaining({
121
+ operationId: expect.any(String),
122
+ }),
117
123
  );
118
124
  });
119
125
  });
@@ -193,17 +199,22 @@ describe('resolve_aborted_tools executor', () => {
193
199
  });
194
200
 
195
201
  // Then
196
- expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith({
197
- role: 'tool',
198
- content: 'Tool execution was aborted by user.',
199
- plugin: toolCall,
200
- pluginIntervention: { status: 'aborted' },
201
- tool_call_id: 'tool_abc',
202
- parentId: 'msg_parent',
203
- sessionId: 'sess_123',
204
- topicId: 'topic_456',
205
- threadId: undefined,
206
- });
202
+ expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith(
203
+ expect.objectContaining({
204
+ role: 'tool',
205
+ content: 'Tool execution was aborted by user.',
206
+ plugin: toolCall,
207
+ pluginIntervention: { status: 'aborted' },
208
+ tool_call_id: 'tool_abc',
209
+ parentId: 'msg_parent',
210
+ sessionId: 'sess_123',
211
+ topicId: 'topic_456',
212
+ threadId: undefined,
213
+ }),
214
+ expect.objectContaining({
215
+ operationId: expect.any(String),
216
+ }),
217
+ );
207
218
  });
208
219
 
209
220
  it('should preserve tool payload details', async () => {
@@ -240,6 +251,9 @@ describe('resolve_aborted_tools executor', () => {
240
251
  expect.objectContaining({
241
252
  plugin: toolCall,
242
253
  }),
254
+ expect.objectContaining({
255
+ operationId: expect.any(String),
256
+ }),
243
257
  );
244
258
  });
245
259
 
@@ -264,6 +278,9 @@ describe('resolve_aborted_tools executor', () => {
264
278
  expect.objectContaining({
265
279
  topicId: undefined,
266
280
  }),
281
+ expect.objectContaining({
282
+ operationId: expect.any(String),
283
+ }),
267
284
  );
268
285
  });
269
286
  });
@@ -481,6 +498,9 @@ describe('resolve_aborted_tools executor', () => {
481
498
  expect.objectContaining({
482
499
  plugin: toolCall,
483
500
  }),
501
+ expect.objectContaining({
502
+ operationId: expect.any(String),
503
+ }),
484
504
  );
485
505
  });
486
506
 
@@ -571,6 +591,9 @@ describe('resolve_aborted_tools executor', () => {
571
591
  type: 'builtin',
572
592
  }),
573
593
  }),
594
+ expect.objectContaining({
595
+ operationId: expect.any(String),
596
+ }),
574
597
  );
575
598
  });
576
599
 
@@ -606,6 +629,9 @@ describe('resolve_aborted_tools executor', () => {
606
629
  type: 'default',
607
630
  }),
608
631
  }),
632
+ expect.objectContaining({
633
+ operationId: expect.any(String),
634
+ }),
609
635
  );
610
636
  });
611
637
 
@@ -89,16 +89,19 @@ export const createAgentExecutors = (context: {
89
89
  llmPayload.parentMessageId = context.parentId;
90
90
  }
91
91
  // Create assistant message (following server-side pattern)
92
- const assistantMessageItem = await context.get().optimisticCreateMessage({
93
- content: LOADING_FLAT,
94
- model: llmPayload.model,
95
- parentId: llmPayload.parentMessageId,
96
- provider: llmPayload.provider,
97
- role: 'assistant',
98
- sessionId: opContext.sessionId!,
99
- threadId: opContext.threadId,
100
- topicId: opContext.topicId ?? undefined,
101
- });
92
+ const assistantMessageItem = await context.get().optimisticCreateMessage(
93
+ {
94
+ content: LOADING_FLAT,
95
+ model: llmPayload.model,
96
+ parentId: llmPayload.parentMessageId,
97
+ provider: llmPayload.provider,
98
+ role: 'assistant',
99
+ sessionId: opContext.sessionId!,
100
+ threadId: opContext.threadId,
101
+ topicId: opContext.topicId ?? undefined,
102
+ },
103
+ { operationId: context.operationId },
104
+ );
102
105
 
103
106
  if (!assistantMessageItem) {
104
107
  throw new Error('Failed to create assistant message');
@@ -371,7 +374,9 @@ export const createAgentExecutors = (context: {
371
374
  topicId: opContext.topicId ?? undefined,
372
375
  };
373
376
 
374
- const createPromise = context.get().optimisticCreateMessage(toolMessageParams);
377
+ const createPromise = context
378
+ .get()
379
+ .optimisticCreateMessage(toolMessageParams, { operationId: createToolMsgOpId });
375
380
  context.get().updateOperationMetadata(createToolMsgOpId, {
376
381
  createMessagePromise: createPromise,
377
382
  });
@@ -632,7 +637,9 @@ export const createAgentExecutors = (context: {
632
637
  topicId: opContext.topicId ?? undefined,
633
638
  };
634
639
 
635
- const createResult = await context.get().optimisticCreateMessage(toolMessageParams);
640
+ const createResult = await context
641
+ .get()
642
+ .optimisticCreateMessage(toolMessageParams, { operationId: context.operationId });
636
643
 
637
644
  if (!createResult) {
638
645
  log(
@@ -709,7 +716,9 @@ export const createAgentExecutors = (context: {
709
716
  topicId: opContext.topicId ?? undefined,
710
717
  };
711
718
 
712
- const createResult = await context.get().optimisticCreateMessage(toolMessageParams);
719
+ const createResult = await context
720
+ .get()
721
+ .optimisticCreateMessage(toolMessageParams, { operationId: context.operationId });
713
722
 
714
723
  if (createResult) {
715
724
  log(
@@ -261,6 +261,10 @@ export const conversationLifecycle: StateCreator<
261
261
 
262
262
  summaryTitle().catch(console.error);
263
263
 
264
+ // Complete sendMessage operation here - message creation is done
265
+ // execAgentRuntime is a separate operation (child) that handles AI response generation
266
+ get().completeOperation(operationId);
267
+
264
268
  // Get the current messages to generate AI response
265
269
  const displayMessages = displayMessageSelectors.activeDisplayMessages(get());
266
270
 
@@ -287,16 +291,8 @@ export const conversationLifecycle: StateCreator<
287
291
  if (userFiles.length > 0) {
288
292
  await getAgentStoreState().addFilesToAgent(userFiles, false);
289
293
  }
290
-
291
- // Complete operation on success
292
- get().completeOperation(operationId);
293
294
  } catch (e) {
294
295
  console.error(e);
295
- // Fail operation on error
296
- get().failOperation(operationId, {
297
- type: e instanceof Error ? e.name : 'unknown_error',
298
- message: e instanceof Error ? e.message : 'AI generation failed',
299
- });
300
296
  } finally {
301
297
  if (data.topicId) get().internal_updateTopicLoading(data.topicId, false);
302
298
  }
@@ -241,9 +241,18 @@ describe('search actions', () => {
241
241
  it('should update arguments and perform search', async () => {
242
242
  const { result } = renderHook(() => useChatStore());
243
243
  const spy = vi.spyOn(result.current, 'search');
244
- const { triggerSearchAgain } = result.current;
245
244
 
246
245
  const messageId = 'test-message-id';
246
+ const operationId = 'op_test';
247
+
248
+ // Set up messageOperationMap so triggerSearchAgain can get operationId
249
+ useChatStore.setState({
250
+ messageOperationMap: {
251
+ [messageId]: operationId,
252
+ },
253
+ });
254
+
255
+ const { triggerSearchAgain } = result.current;
247
256
  const query: SearchQuery = {
248
257
  query: 'test query',
249
258
  };
@@ -252,7 +261,12 @@ describe('search actions', () => {
252
261
  await triggerSearchAgain(messageId, query, { aiSummary: true });
253
262
  });
254
263
 
255
- expect(result.current.optimisticUpdatePluginArguments).toHaveBeenCalledWith(messageId, query);
264
+ expect(result.current.optimisticUpdatePluginArguments).toHaveBeenCalledWith(
265
+ messageId,
266
+ query,
267
+ false,
268
+ { operationId },
269
+ );
256
270
  expect(spy).toHaveBeenCalledWith(messageId, query, true);
257
271
  });
258
272
  });