@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.
- package/CHANGELOG.md +25 -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 +9 -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/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
|
@@ -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
|
-
*
|
|
49
|
+
* Clean up completed or cancelled operations
|
|
50
|
+
* Removes operations that are older than the specified age (default: 30 seconds)
|
|
47
51
|
*/
|
|
48
|
-
cleanupCompletedOperations: (
|
|
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)
|
|
385
|
+
if (!operation) {
|
|
386
|
+
log('[cancelOperation] operation not found: %s', operationId);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
247
389
|
|
|
248
|
-
//
|
|
249
|
-
if (operation.
|
|
250
|
-
operation.
|
|
251
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
169
|
-
*
|
|
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
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
| '
|
|
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
|