@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
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable sort-keys-fix/sort-keys-fix */
2
2
  import { nanoid } from '@lobechat/utils';
3
+ import debug from 'debug';
3
4
  import { produce } from 'immer';
4
5
  import { StateCreator } from 'zustand/vanilla';
5
6
 
@@ -9,6 +10,7 @@ import { setNamespace } from '@/utils/storeDebug';
9
10
 
10
11
  import type {
11
12
  Operation,
13
+ OperationCancelContext,
12
14
  OperationContext,
13
15
  OperationFilter,
14
16
  OperationMetadata,
@@ -17,6 +19,7 @@ import type {
17
19
  } from './types';
18
20
 
19
21
  const n = setNamespace('operation');
22
+ const log = debug('lobe-store:operation');
20
23
 
21
24
  /**
22
25
  * Operation Actions
@@ -43,9 +46,10 @@ export interface OperationActions {
43
46
  cancelOperations: (filter: OperationFilter, reason?: string) => string[];
44
47
 
45
48
  /**
46
- * Cleanup completed operations (prevent memory leak)
49
+ * Clean up completed or cancelled operations
50
+ * Removes operations that are older than the specified age (default: 30 seconds)
47
51
  */
48
- cleanupCompletedOperations: (olderThan?: number) => void;
52
+ cleanupCompletedOperations: (maxAgeMs?: number) => number;
49
53
 
50
54
  /**
51
55
  * Complete operation
@@ -60,6 +64,29 @@ export interface OperationActions {
60
64
  error: { code?: string; details?: any; message: string; type: string },
61
65
  ) => void;
62
66
 
67
+ /**
68
+ * Get operation's AbortSignal (for passing to async operations like fetch)
69
+ */
70
+ getOperationAbortSignal: (operationId: string) => AbortSignal;
71
+
72
+ /**
73
+ * Get sessionId and topicId from operation or fallback to global state
74
+ * This is a helper method that can be used by other slices
75
+ */
76
+ internal_getSessionContext: (context?: { operationId?: string }) => {
77
+ sessionId: string;
78
+ topicId: string | null | undefined;
79
+ };
80
+
81
+ /**
82
+ * Register cancel handler for an operation
83
+ * The handler will be called when the operation is cancelled
84
+ */
85
+ onOperationCancel: (
86
+ operationId: string,
87
+ handler: (context: OperationCancelContext) => void | Promise<void>,
88
+ ) => void;
89
+
63
90
  /**
64
91
  * Start an operation (supports auto-inheriting context from parent operation)
65
92
  */
@@ -72,6 +99,11 @@ export interface OperationActions {
72
99
  type: OperationType;
73
100
  }) => { abortController: AbortController; operationId: string };
74
101
 
102
+ /**
103
+ * Update operation metadata
104
+ */
105
+ updateOperationMetadata: (operationId: string, metadata: Partial<OperationMetadata>) => void;
106
+
75
107
  /**
76
108
  * Update operation progress
77
109
  */
@@ -93,6 +125,35 @@ export const operationActions: StateCreator<
93
125
  [],
94
126
  OperationActions
95
127
  > = (set, get) => ({
128
+ internal_getSessionContext: (context) => {
129
+ if (context?.operationId) {
130
+ const operation = get().operations[context.operationId];
131
+ if (!operation) {
132
+ log('[internal_getSessionContext] ERROR: Operation not found: %s', context.operationId);
133
+ throw new Error(`Operation not found: ${context.operationId}`);
134
+ }
135
+ const sessionId = operation.context.sessionId!;
136
+ const topicId = operation.context.topicId;
137
+ log(
138
+ '[internal_getSessionContext] get from operation %s: sessionId=%s, topicId=%s',
139
+ context.operationId,
140
+ sessionId,
141
+ topicId,
142
+ );
143
+ return { sessionId, topicId };
144
+ }
145
+
146
+ // Fallback to global state
147
+ const sessionId = get().activeId;
148
+ const topicId = get().activeTopicId;
149
+ log(
150
+ '[internal_getSessionContext] use global state: sessionId=%s, topicId=%s',
151
+ sessionId,
152
+ topicId,
153
+ );
154
+ return { sessionId, topicId };
155
+ },
156
+
96
157
  startOperation: (params) => {
97
158
  const {
98
159
  type,
@@ -113,9 +174,12 @@ export const operationActions: StateCreator<
113
174
  if (parentOp) {
114
175
  // Inherit parent's context, allow partial override
115
176
  context = { ...parentOp.context, ...partialContext };
177
+ log('[startOperation] inherit context from parent %s: %o', parentOperationId, context);
116
178
  }
117
179
  }
118
180
 
181
+ log('[startOperation] create operation %s (type=%s, context=%o)', operationId, type, context);
182
+
119
183
  const abortController = new AbortController();
120
184
  const now = Date.now();
121
185
 
@@ -152,6 +216,10 @@ export const operationActions: StateCreator<
152
216
  state.operationsByMessage[context.messageId] = [];
153
217
  }
154
218
  state.operationsByMessage[context.messageId].push(operationId);
219
+
220
+ // Auto-associate message with this operation (most granular)
221
+ // This allows tools to access the correct AbortController via messageOperationMap
222
+ state.messageOperationMap[context.messageId] = operationId;
155
223
  }
156
224
 
157
225
  // Update context index (if sessionId exists)
@@ -178,9 +246,41 @@ export const operationActions: StateCreator<
178
246
  n(`startOperation/${type}/${operationId}`),
179
247
  );
180
248
 
249
+ // Periodically cleanup old completed operations
250
+ // Only cleanup for top-level operations (no parent) to avoid excessive cleanup calls
251
+ if (!parentOperationId) {
252
+ // Clean up operations completed more than 30 seconds ago
253
+ get().cleanupCompletedOperations(30_000);
254
+ }
255
+
181
256
  return { operationId, abortController };
182
257
  },
183
258
 
259
+ updateOperationMetadata: (operationId, metadata) => {
260
+ const operation = get().operations[operationId];
261
+ if (metadata.isAborting) {
262
+ log(
263
+ '[updateOperationMetadata] Setting isAborting=true for operation %s (type=%s)',
264
+ operationId,
265
+ operation?.type,
266
+ );
267
+ }
268
+
269
+ set(
270
+ produce((state: ChatStore) => {
271
+ const operation = state.operations[operationId];
272
+ if (!operation) return;
273
+
274
+ operation.metadata = {
275
+ ...operation.metadata,
276
+ ...metadata,
277
+ };
278
+ }),
279
+ false,
280
+ n(`updateOperationMetadata/${operationId}`),
281
+ );
282
+ },
283
+
184
284
  updateOperationStatus: (operationId, status, metadata) => {
185
285
  set(
186
286
  produce((state: ChatStore) => {
@@ -219,6 +319,16 @@ export const operationActions: StateCreator<
219
319
  },
220
320
 
221
321
  completeOperation: (operationId, metadata) => {
322
+ const operation = get().operations[operationId];
323
+ if (operation) {
324
+ log(
325
+ '[completeOperation] operation %s (type=%s) completed, duration=%dms',
326
+ operationId,
327
+ operation.type,
328
+ Date.now() - operation.metadata.startTime,
329
+ );
330
+ }
331
+
222
332
  set(
223
333
  produce((state: ChatStore) => {
224
334
  const operation = state.operations[operationId];
@@ -241,25 +351,92 @@ export const operationActions: StateCreator<
241
351
  );
242
352
  },
243
353
 
354
+ getOperationAbortSignal: (operationId) => {
355
+ const operation = get().operations[operationId];
356
+ if (!operation) {
357
+ throw new Error(`[getOperationAbortSignal] Operation not found: ${operationId}`);
358
+ }
359
+ return operation.abortController.signal;
360
+ },
361
+
362
+ onOperationCancel: (operationId, handler) => {
363
+ set(
364
+ produce((state: ChatStore) => {
365
+ const operation = state.operations[operationId];
366
+ if (!operation) {
367
+ log('[onOperationCancel] WARNING: Operation not found: %s', operationId);
368
+ return;
369
+ }
370
+
371
+ operation.onCancelHandler = handler;
372
+ log(
373
+ '[onOperationCancel] registered cancel handler for %s (type=%s)',
374
+ operationId,
375
+ operation.type,
376
+ );
377
+ }),
378
+ false,
379
+ n(`onOperationCancel/${operationId}`),
380
+ );
381
+ },
382
+
244
383
  cancelOperation: (operationId, reason = 'User cancelled') => {
245
384
  const operation = get().operations[operationId];
246
- if (!operation) return;
385
+ if (!operation) {
386
+ log('[cancelOperation] operation not found: %s', operationId);
387
+ return;
388
+ }
247
389
 
248
- // Cancel all child operations recursively
249
- if (operation.childOperationIds && operation.childOperationIds.length > 0) {
250
- operation.childOperationIds.forEach((childId) => {
251
- get().cancelOperation(childId, 'Parent operation cancelled');
252
- });
390
+ // Skip if already cancelled or completed
391
+ if (operation.status === 'cancelled' || operation.status === 'completed') {
392
+ log('[cancelOperation] operation %s already %s, skipping', operationId, operation.status);
393
+ return;
253
394
  }
254
395
 
255
- // Abort the operation
396
+ log(
397
+ '[cancelOperation] cancelling operation %s (type=%s), reason: %s',
398
+ operationId,
399
+ operation.type,
400
+ reason,
401
+ );
402
+
403
+ // 1. Abort the operation (triggers AbortSignal for all async operations)
256
404
  try {
257
405
  operation.abortController.abort(reason);
258
406
  } catch {
259
407
  // Ignore abort errors
260
408
  }
261
409
 
262
- // Update status
410
+ // 2. Set isAborting flag immediately for execAgentRuntime operations
411
+ // This ensures UI (loading button) responds instantly to user cancellation
412
+ if (operation.type === 'execAgentRuntime') {
413
+ get().updateOperationMetadata(operationId, { isAborting: true });
414
+ }
415
+
416
+ // 3. Call cancel handler if registered
417
+ if (operation.onCancelHandler) {
418
+ log('[cancelOperation] calling cancel handler for %s (type=%s)', operationId, operation.type);
419
+
420
+ const cancelContext: OperationCancelContext = {
421
+ operationId,
422
+ type: operation.type,
423
+ reason,
424
+ metadata: operation.metadata,
425
+ };
426
+
427
+ // Execute handler asynchronously (don't block cancellation flow)
428
+ // Use try-catch to handle synchronous errors, then wrap in Promise for async errors
429
+ try {
430
+ Promise.resolve(operation.onCancelHandler(cancelContext)).catch((err) => {
431
+ log('[cancelOperation] cancel handler error for %s: %O', operationId, err);
432
+ });
433
+ } catch (err) {
434
+ // Handle synchronous errors from handler
435
+ log('[cancelOperation] cancel handler synchronous error for %s: %O', operationId, err);
436
+ }
437
+ }
438
+
439
+ // 4. Update status
263
440
  set(
264
441
  produce((state: ChatStore) => {
265
442
  const op = state.operations[operationId];
@@ -274,9 +451,27 @@ export const operationActions: StateCreator<
274
451
  false,
275
452
  n(`cancelOperation/${operationId}`),
276
453
  );
454
+
455
+ // 4. Cancel all child operations recursively
456
+ if (operation.childOperationIds && operation.childOperationIds.length > 0) {
457
+ log('[cancelOperation] cancelling %d child operations', operation.childOperationIds.length);
458
+ operation.childOperationIds.forEach((childId) => {
459
+ get().cancelOperation(childId, 'Parent operation cancelled');
460
+ });
461
+ }
277
462
  },
278
463
 
279
464
  failOperation: (operationId, error) => {
465
+ const operation = get().operations[operationId];
466
+ if (operation) {
467
+ log(
468
+ '[failOperation] operation %s (type=%s) failed: %s',
469
+ operationId,
470
+ operation.type,
471
+ error.message,
472
+ );
473
+ }
474
+
280
475
  set(
281
476
  produce((state: ChatStore) => {
282
477
  const operation = state.operations[operationId];
@@ -373,7 +568,7 @@ export const operationActions: StateCreator<
373
568
  }
374
569
  });
375
570
 
376
- if (operationsToDelete.length === 0) return;
571
+ if (operationsToDelete.length === 0) return 0;
377
572
 
378
573
  set(
379
574
  produce((state: ChatStore) => {
@@ -437,12 +632,24 @@ export const operationActions: StateCreator<
437
632
  false,
438
633
  n(`cleanupCompletedOperations/count=${operationsToDelete.length}`),
439
634
  );
635
+
636
+ log('[cleanupCompletedOperations] cleaned up %d operations', operationsToDelete.length);
637
+ return operationsToDelete.length;
440
638
  },
441
639
 
442
640
  associateMessageWithOperation: (messageId, operationId) => {
443
641
  set(
444
642
  produce((state: ChatStore) => {
643
+ // Update messageOperationMap (for single operation lookup)
445
644
  state.messageOperationMap[messageId] = operationId;
645
+
646
+ // Update operationsByMessage index (for multiple operations lookup)
647
+ if (!state.operationsByMessage[messageId]) {
648
+ state.operationsByMessage[messageId] = [];
649
+ }
650
+ if (!state.operationsByMessage[messageId].includes(operationId)) {
651
+ state.operationsByMessage[messageId].push(operationId);
652
+ }
446
653
  }),
447
654
  false,
448
655
  n(`associateMessageWithOperation/${messageId}/${operationId}`),
@@ -165,11 +165,30 @@ const getCurrentOperationProgress = (s: ChatStoreState): number | undefined => {
165
165
 
166
166
  // === Backward Compatibility ===
167
167
  /**
168
- * Check if AI is generating (for backward compatibility)
169
- * Equivalent to: hasRunningOperationType('generateAI')
168
+ * Check if agent runtime is running (including both main window and thread)
169
+ * Excludes operations that are aborting (cleaning up after cancellation)
170
170
  */
171
- const isAIGenerating = (s: ChatStoreState): boolean => {
172
- return hasRunningOperationType('generateAI')(s);
171
+ const isAgentRuntimeRunning = (s: ChatStoreState): boolean => {
172
+ const operationIds = s.operationsByType['execAgentRuntime'] || [];
173
+ return operationIds.some((id) => {
174
+ const op = s.operations[id];
175
+ // Exclude operations that are aborting (user already cancelled, just cleaning up)
176
+ return op && op.status === 'running' && !op.metadata.isAborting;
177
+ });
178
+ };
179
+
180
+ /**
181
+ * Check if agent runtime is running in main window only
182
+ * Used for main window UI state (e.g., send button loading)
183
+ * Excludes thread operations to prevent cross-contamination
184
+ */
185
+ const isMainWindowAgentRuntimeRunning = (s: ChatStoreState): boolean => {
186
+ const operationIds = s.operationsByType['execAgentRuntime'] || [];
187
+ return operationIds.some((id) => {
188
+ const op = s.operations[id];
189
+ // Only include main window operations (not thread)
190
+ return op && op.status === 'running' && !op.metadata.isAborting && !op.metadata.inThread;
191
+ });
173
192
  };
174
193
 
175
194
  /**
@@ -194,7 +213,7 @@ const isInSearchWorkflow = (s: ChatStoreState): boolean => {
194
213
  };
195
214
 
196
215
  /**
197
- * Check if a specific message is being processed
216
+ * Check if a specific message is being processed (any operation type)
198
217
  */
199
218
  const isMessageProcessing =
200
219
  (messageId: string) =>
@@ -203,6 +222,102 @@ const isMessageProcessing =
203
222
  return operations.some((op) => op.status === 'running');
204
223
  };
205
224
 
225
+ /**
226
+ * Check if a specific message is being generated (AI generation only)
227
+ * This is more specific than isMessageProcessing - only checks execAgentRuntime operations
228
+ */
229
+ const isMessageGenerating =
230
+ (messageId: string) =>
231
+ (s: ChatStoreState): boolean => {
232
+ const operations = getOperationsByMessage(messageId)(s);
233
+ return operations.some((op) => op.type === 'execAgentRuntime' && op.status === 'running');
234
+ };
235
+
236
+ /**
237
+ * Check if a specific message is being created (CRUD operation only)
238
+ * Checks message creation operations:
239
+ * - User messages: sendMessage
240
+ * - Assistant messages: createAssistantMessage
241
+ */
242
+ const isMessageCreating =
243
+ (messageId: string) =>
244
+ (s: ChatStoreState): boolean => {
245
+ const operations = getOperationsByMessage(messageId)(s);
246
+ return operations.some(
247
+ (op) =>
248
+ (op.type === 'sendMessage' || op.type === 'createAssistantMessage') &&
249
+ op.status === 'running',
250
+ );
251
+ };
252
+
253
+ /**
254
+ * Check if any message in a list is being processed
255
+ */
256
+ const isAnyMessageLoading =
257
+ (messageIds: string[]) =>
258
+ (s: ChatStoreState): boolean => {
259
+ return messageIds.some((id) => isMessageProcessing(id)(s));
260
+ };
261
+
262
+ /**
263
+ * Check if a specific message is being regenerated
264
+ */
265
+ const isMessageRegenerating =
266
+ (messageId: string) =>
267
+ (s: ChatStoreState): boolean => {
268
+ const operations = getOperationsByMessage(messageId)(s);
269
+ return operations.some((op) => op.type === 'regenerate' && op.status === 'running');
270
+ };
271
+
272
+ /**
273
+ * Check if a specific message is continuing generation
274
+ */
275
+ const isMessageContinuing =
276
+ (messageId: string) =>
277
+ (s: ChatStoreState): boolean => {
278
+ const operations = getOperationsByMessage(messageId)(s);
279
+ return operations.some((op) => op.type === 'continue' && op.status === 'running');
280
+ };
281
+
282
+ /**
283
+ * Check if a specific message is in reasoning state
284
+ */
285
+ const isMessageInReasoning =
286
+ (messageId: string) =>
287
+ (s: ChatStoreState): boolean => {
288
+ const operations = getOperationsByMessage(messageId)(s);
289
+ return operations.some((op) => op.type === 'reasoning' && op.status === 'running');
290
+ };
291
+
292
+ /**
293
+ * Check if a specific message is in tool calling (plugin API invocation)
294
+ */
295
+ const isMessageInToolCalling =
296
+ (messageId: string) =>
297
+ (s: ChatStoreState): boolean => {
298
+ const operations = getOperationsByMessage(messageId)(s);
299
+ return operations.some((op) => op.type === 'toolCalling' && op.status === 'running');
300
+ };
301
+
302
+ /**
303
+ * Check if currently aborting (cleaning up after user cancellation)
304
+ * Used to show "Cleaning up tool calls..." message
305
+ */
306
+ const isAborting = (s: ChatStoreState): boolean => {
307
+ const currentOps = getCurrentContextOperations(s);
308
+ return currentOps.some((op) => op.status === 'running' && op.metadata.isAborting);
309
+ };
310
+
311
+ /**
312
+ * Check if a specific message is aborting
313
+ */
314
+ const isMessageAborting =
315
+ (messageId: string) =>
316
+ (s: ChatStoreState): boolean => {
317
+ const operations = getOperationsByMessage(messageId)(s);
318
+ return operations.some((op) => op.status === 'running' && op.metadata.isAborting);
319
+ };
320
+
206
321
  /**
207
322
  * Check if regenerating (for backward compatibility)
208
323
  */
@@ -236,11 +351,25 @@ export const operationSelectors = {
236
351
  getRunningOperations,
237
352
  hasAnyRunningOperation,
238
353
  hasRunningOperationType,
239
- isAIGenerating,
354
+ /** @deprecated Use isAgentRuntimeRunning instead */
355
+ isAIGenerating: isAgentRuntimeRunning,
356
+
357
+ isAborting,
358
+
359
+ isAgentRuntimeRunning,
360
+ isAnyMessageLoading,
240
361
  isContinuing,
241
362
  isInRAGFlow,
242
363
  isInSearchWorkflow,
364
+ isMainWindowAgentRuntimeRunning,
365
+ isMessageAborting,
366
+ isMessageContinuing,
367
+ isMessageCreating,
368
+ isMessageGenerating,
369
+ isMessageInReasoning,
370
+ isMessageInToolCalling,
243
371
  isMessageProcessing,
372
+ isMessageRegenerating,
244
373
  isRegenerating,
245
374
  isSendingMessage,
246
375
  };
@@ -11,12 +11,16 @@ export type OperationType =
11
11
  // === Message sending ===
12
12
  | 'sendMessage' // Send message to server
13
13
  | 'createTopic' // Auto create topic
14
+ | 'regenerate' // Regenerate message
15
+ | 'continue' // Continue generation
14
16
 
15
17
  // === AI generation ===
16
- | 'generateAI' // AI generate response (entire agent runtime execution)
18
+ | 'execAgentRuntime' // Execute agent runtime (entire agent runtime execution)
19
+ | 'createAssistantMessage' // Create assistant message (sub-operation of execAgentRuntime)
20
+ // === LLM execution (sub-operations) ===
21
+ | 'callLLM' // Call LLM streaming response (sub-operation of execAgentRuntime)
22
+ // === (sub-operations) ===
17
23
  | 'reasoning' // AI reasoning process (child operation)
18
- | 'regenerate' // Regenerate message
19
- | 'continue' // Continue generation
20
24
 
21
25
  // === RAG and retrieval ===
22
26
  | 'rag' // RAG retrieval flow (child operation)
@@ -24,6 +28,10 @@ export type OperationType =
24
28
 
25
29
  // === Tool calling ===
26
30
  | 'toolCalling' // Tool calling (streaming, child operation)
31
+ // === (sub-operations) ===
32
+ | 'createToolMessage' // Create tool message (sub-operation of executeToolCall)
33
+ | 'executeToolCall' // Execute tool call (sub-operation of toolCalling)
34
+ // === (sub-operations of executeToolCall) ===
27
35
  | 'pluginApi' // Plugin API call
28
36
  | 'builtinToolSearch' // Builtin tool: search
29
37
  | 'builtinToolInterpreter' // Builtin tool: code interpreter
@@ -34,6 +42,7 @@ export type OperationType =
34
42
  | 'groupAgentGenerate' // Group agent generate
35
43
 
36
44
  // === Others ===
45
+ | 'translate' // Translate message
37
46
  | 'topicSummary' // Topic summary
38
47
  | 'historySummary'; // History summary
39
48
 
@@ -61,6 +70,16 @@ export interface OperationContext {
61
70
  agentId?: string; // Associated agent ID (specific agent in Group Chat)
62
71
  }
63
72
 
73
+ /**
74
+ * Operation cancel context - passed to cancel handler
75
+ */
76
+ export interface OperationCancelContext {
77
+ operationId: string;
78
+ type: OperationType;
79
+ reason: string;
80
+ metadata?: OperationMetadata;
81
+ }
82
+
64
83
  /**
65
84
  * Operation metadata
66
85
  */
@@ -88,6 +107,10 @@ export interface OperationMetadata {
88
107
  // Cancel information
89
108
  cancelReason?: string;
90
109
 
110
+ // UI state (for sendMessage operation)
111
+ inputEditorTempState?: any | null; // Editor state snapshot for cancel restoration
112
+ inputSendErrorMsg?: string; // Error message to display in UI
113
+
91
114
  // Other metadata (extensible)
92
115
  [key: string]: any;
93
116
  }
@@ -110,6 +133,9 @@ export interface Operation {
110
133
  // === Metadata ===
111
134
  metadata: OperationMetadata;
112
135
 
136
+ // === Cancel handler ===
137
+ onCancelHandler?: (context: OperationCancelContext) => void | Promise<void>; // Cancel callback
138
+
113
139
  // === Dependencies ===
114
140
  parentOperationId?: string; // Parent operation ID (for operation nesting)
115
141
  childOperationIds?: string[]; // Child operation IDs