@lobehub/lobehub 2.0.0-next.104 → 2.0.0-next.105
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/package.json +2 -2
- package/changelog/v1.json +5 -0
- package/package.json +9 -3
- package/packages/database/src/repositories/knowledge/index.ts +5 -8
- package/packages/model-bank/src/aiModels/moonshot.ts +46 -0
- package/packages/model-runtime/src/core/contextBuilders/openai.ts +1 -1
- package/packages/model-runtime/src/providers/moonshot/index.ts +17 -4
- package/packages/types/src/user/settings/keyVaults.ts +0 -68
- package/packages/utils/src/client/parserPlaceholder.ts +1 -1
- package/src/services/__tests__/_auth.test.ts +1 -4
- package/src/services/_auth.ts +2 -3
- package/src/services/_header.ts +1 -8
- package/src/store/chat/agents/__tests__/createAgentExecutors/call-llm.test.ts +18 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts +40 -11
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/assertions.ts +3 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/request-human-approve.test.ts +15 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/resolve-aborted-tools.test.ts +37 -11
- package/src/store/chat/agents/createAgentExecutors.ts +22 -13
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +4 -8
- package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +16 -2
- package/src/store/chat/slices/builtinTool/actions/localSystem.ts +5 -1
- package/src/store/chat/slices/builtinTool/actions/search.ts +5 -1
- package/src/store/chat/slices/message/actions/publicApi.ts +10 -2
- package/src/store/chat/slices/message/actions/query.ts +17 -4
- package/src/store/chat/slices/operation/__tests__/selectors.test.ts +93 -5
- package/src/store/chat/slices/operation/selectors.ts +16 -3
- package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +24 -18
- package/src/store/user/slices/settings/selectors/keyVaults.ts +0 -5
- package/src/features/ChatList/Error/AccessCodeForm.tsx +0 -63
- package/src/services/__tests__/share.test.ts +0 -61
|
@@ -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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
});
|
|
@@ -108,7 +108,11 @@ export const localSystemSlice: StateCreator<
|
|
|
108
108
|
},
|
|
109
109
|
|
|
110
110
|
reSearchLocalFiles: async (id, params) => {
|
|
111
|
-
|
|
111
|
+
// Get operationId from messageOperationMap to ensure proper context isolation
|
|
112
|
+
const operationId = get().messageOperationMap[id];
|
|
113
|
+
const context = operationId ? { operationId } : undefined;
|
|
114
|
+
|
|
115
|
+
await get().optimisticUpdatePluginArguments(id, params, false, context);
|
|
112
116
|
|
|
113
117
|
return get().searchLocalFiles(id, params);
|
|
114
118
|
},
|
|
@@ -276,7 +276,11 @@ export const searchSlice: StateCreator<
|
|
|
276
276
|
},
|
|
277
277
|
|
|
278
278
|
triggerSearchAgain: async (id, data, options) => {
|
|
279
|
-
|
|
279
|
+
// Get operationId from messageOperationMap to ensure proper context isolation
|
|
280
|
+
const operationId = get().messageOperationMap[id];
|
|
281
|
+
const context = operationId ? { operationId } : undefined;
|
|
282
|
+
|
|
283
|
+
await get().optimisticUpdatePluginArguments(id, data, false, context);
|
|
280
284
|
|
|
281
285
|
await get().search(id, data, options?.aiSummary);
|
|
282
286
|
},
|
|
@@ -167,14 +167,22 @@ export const messagePublicApi: StateCreator<
|
|
|
167
167
|
const message = dbMessageSelectors.getDbMessageById(id)(get());
|
|
168
168
|
if (!message || message.role !== 'tool') return;
|
|
169
169
|
|
|
170
|
+
// Get operationId from messageOperationMap to ensure proper context isolation
|
|
171
|
+
const operationId = get().messageOperationMap[id];
|
|
172
|
+
const context = operationId ? { operationId } : undefined;
|
|
173
|
+
|
|
170
174
|
const removeToolInAssistantMessage = async () => {
|
|
171
175
|
if (!message.parentId) return;
|
|
172
|
-
await get().optimisticRemoveToolFromAssistantMessage(
|
|
176
|
+
await get().optimisticRemoveToolFromAssistantMessage(
|
|
177
|
+
message.parentId,
|
|
178
|
+
message.tool_call_id,
|
|
179
|
+
context,
|
|
180
|
+
);
|
|
173
181
|
};
|
|
174
182
|
|
|
175
183
|
await Promise.all([
|
|
176
184
|
// 1. remove tool message
|
|
177
|
-
get().optimisticDeleteMessage(id),
|
|
185
|
+
get().optimisticDeleteMessage(id, context),
|
|
178
186
|
// 2. remove the tool item in the assistant tools
|
|
179
187
|
removeToolInAssistantMessage(),
|
|
180
188
|
]);
|
|
@@ -32,6 +32,7 @@ export interface MessageQueryAction {
|
|
|
32
32
|
messages: UIChatMessage[],
|
|
33
33
|
params?: {
|
|
34
34
|
action?: any;
|
|
35
|
+
operationId?: string;
|
|
35
36
|
sessionId?: string;
|
|
36
37
|
topicId?: string | null;
|
|
37
38
|
},
|
|
@@ -66,10 +67,22 @@ export const messageQuery: StateCreator<
|
|
|
66
67
|
},
|
|
67
68
|
|
|
68
69
|
replaceMessages: (messages, params) => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
let sessionId: string;
|
|
71
|
+
let topicId: string | null | undefined;
|
|
72
|
+
|
|
73
|
+
// Priority 1: Get context from operation if operationId is provided
|
|
74
|
+
if (params?.operationId) {
|
|
75
|
+
const { sessionId: opSessionId, topicId: opTopicId } =
|
|
76
|
+
get().internal_getSessionContext(params);
|
|
77
|
+
sessionId = opSessionId;
|
|
78
|
+
topicId = opTopicId;
|
|
79
|
+
} else {
|
|
80
|
+
// Priority 2: Use explicit sessionId/topicId or fallback to global state
|
|
81
|
+
sessionId = params?.sessionId ?? get().activeId;
|
|
82
|
+
topicId = params?.topicId ?? get().activeTopicId;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const messagesKey = messageMapKey(sessionId, topicId);
|
|
73
86
|
|
|
74
87
|
// Get raw messages from dbMessagesMap and apply reducer
|
|
75
88
|
const nextDbMap = { ...get().dbMessagesMap, [messagesKey]: messages };
|
|
@@ -393,6 +393,11 @@ describe('Operation Selectors', () => {
|
|
|
393
393
|
it('isMainWindowAgentRuntimeRunning should only detect main window operations', () => {
|
|
394
394
|
const { result } = renderHook(() => useChatStore());
|
|
395
395
|
|
|
396
|
+
// Set active context
|
|
397
|
+
act(() => {
|
|
398
|
+
useChatStore.setState({ activeId: 'session1', activeTopicId: undefined });
|
|
399
|
+
});
|
|
400
|
+
|
|
396
401
|
expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(false);
|
|
397
402
|
|
|
398
403
|
// Start a main window operation (inThread: false)
|
|
@@ -400,7 +405,7 @@ describe('Operation Selectors', () => {
|
|
|
400
405
|
act(() => {
|
|
401
406
|
mainOpId = result.current.startOperation({
|
|
402
407
|
type: 'execAgentRuntime',
|
|
403
|
-
context: { sessionId: 'session1' },
|
|
408
|
+
context: { sessionId: 'session1', topicId: null },
|
|
404
409
|
metadata: { inThread: false },
|
|
405
410
|
}).operationId;
|
|
406
411
|
});
|
|
@@ -420,12 +425,17 @@ describe('Operation Selectors', () => {
|
|
|
420
425
|
it('isMainWindowAgentRuntimeRunning should exclude thread operations', () => {
|
|
421
426
|
const { result } = renderHook(() => useChatStore());
|
|
422
427
|
|
|
428
|
+
// Set active context
|
|
429
|
+
act(() => {
|
|
430
|
+
useChatStore.setState({ activeId: 'session1', activeTopicId: undefined });
|
|
431
|
+
});
|
|
432
|
+
|
|
423
433
|
// Start a thread operation (inThread: true)
|
|
424
434
|
let threadOpId: string;
|
|
425
435
|
act(() => {
|
|
426
436
|
threadOpId = result.current.startOperation({
|
|
427
437
|
type: 'execAgentRuntime',
|
|
428
|
-
context: { sessionId: 'session1', threadId: 'thread1' },
|
|
438
|
+
context: { sessionId: 'session1', topicId: null, threadId: 'thread1' },
|
|
429
439
|
metadata: { inThread: true },
|
|
430
440
|
}).operationId;
|
|
431
441
|
});
|
|
@@ -447,6 +457,11 @@ describe('Operation Selectors', () => {
|
|
|
447
457
|
it('isMainWindowAgentRuntimeRunning should distinguish between main and thread operations', () => {
|
|
448
458
|
const { result } = renderHook(() => useChatStore());
|
|
449
459
|
|
|
460
|
+
// Set active context
|
|
461
|
+
act(() => {
|
|
462
|
+
useChatStore.setState({ activeId: 'session1', activeTopicId: undefined });
|
|
463
|
+
});
|
|
464
|
+
|
|
450
465
|
let mainOpId: string;
|
|
451
466
|
let threadOpId: string;
|
|
452
467
|
|
|
@@ -454,13 +469,13 @@ describe('Operation Selectors', () => {
|
|
|
454
469
|
act(() => {
|
|
455
470
|
mainOpId = result.current.startOperation({
|
|
456
471
|
type: 'execAgentRuntime',
|
|
457
|
-
context: { sessionId: 'session1' },
|
|
472
|
+
context: { sessionId: 'session1', topicId: null },
|
|
458
473
|
metadata: { inThread: false },
|
|
459
474
|
}).operationId;
|
|
460
475
|
|
|
461
476
|
threadOpId = result.current.startOperation({
|
|
462
477
|
type: 'execAgentRuntime',
|
|
463
|
-
context: { sessionId: 'session1', threadId: 'thread1' },
|
|
478
|
+
context: { sessionId: 'session1', topicId: null, threadId: 'thread1' },
|
|
464
479
|
metadata: { inThread: true },
|
|
465
480
|
}).operationId;
|
|
466
481
|
});
|
|
@@ -489,11 +504,16 @@ describe('Operation Selectors', () => {
|
|
|
489
504
|
it('isMainWindowAgentRuntimeRunning should exclude aborting operations', () => {
|
|
490
505
|
const { result } = renderHook(() => useChatStore());
|
|
491
506
|
|
|
507
|
+
// Set active context
|
|
508
|
+
act(() => {
|
|
509
|
+
useChatStore.setState({ activeId: 'session1', activeTopicId: undefined });
|
|
510
|
+
});
|
|
511
|
+
|
|
492
512
|
let opId: string;
|
|
493
513
|
act(() => {
|
|
494
514
|
opId = result.current.startOperation({
|
|
495
515
|
type: 'execAgentRuntime',
|
|
496
|
-
context: { sessionId: 'session1' },
|
|
516
|
+
context: { sessionId: 'session1', topicId: null },
|
|
497
517
|
metadata: { inThread: false },
|
|
498
518
|
}).operationId;
|
|
499
519
|
});
|
|
@@ -509,5 +529,73 @@ describe('Operation Selectors', () => {
|
|
|
509
529
|
expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(false);
|
|
510
530
|
expect(operationSelectors.isAgentRuntimeRunning(result.current)).toBe(false);
|
|
511
531
|
});
|
|
532
|
+
|
|
533
|
+
it('isMainWindowAgentRuntimeRunning should only detect operations in current active topic', () => {
|
|
534
|
+
const { result } = renderHook(() => useChatStore());
|
|
535
|
+
|
|
536
|
+
// Set active session and topic
|
|
537
|
+
act(() => {
|
|
538
|
+
useChatStore.setState({ activeId: 'session1', activeTopicId: 'topic1' });
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
let topic1OpId: string;
|
|
542
|
+
let topic2OpId: string;
|
|
543
|
+
|
|
544
|
+
// Start operation in topic1 (current active topic)
|
|
545
|
+
act(() => {
|
|
546
|
+
topic1OpId = result.current.startOperation({
|
|
547
|
+
type: 'execAgentRuntime',
|
|
548
|
+
context: { sessionId: 'session1', topicId: 'topic1' },
|
|
549
|
+
metadata: { inThread: false },
|
|
550
|
+
}).operationId;
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// Should detect operation in current topic
|
|
554
|
+
expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(true);
|
|
555
|
+
|
|
556
|
+
// Start operation in topic2 (different topic)
|
|
557
|
+
act(() => {
|
|
558
|
+
topic2OpId = result.current.startOperation({
|
|
559
|
+
type: 'execAgentRuntime',
|
|
560
|
+
context: { sessionId: 'session1', topicId: 'topic2' },
|
|
561
|
+
metadata: { inThread: false },
|
|
562
|
+
}).operationId;
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Should still only detect topic1 operation (current active topic)
|
|
566
|
+
expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(true);
|
|
567
|
+
|
|
568
|
+
// Switch to topic2
|
|
569
|
+
act(() => {
|
|
570
|
+
useChatStore.setState({ activeTopicId: 'topic2' });
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Should now detect topic2 operation
|
|
574
|
+
expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(true);
|
|
575
|
+
|
|
576
|
+
// Complete topic2 operation
|
|
577
|
+
act(() => {
|
|
578
|
+
result.current.completeOperation(topic2OpId!);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// Should not detect any operation in topic2 now
|
|
582
|
+
expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(false);
|
|
583
|
+
|
|
584
|
+
// Switch back to topic1
|
|
585
|
+
act(() => {
|
|
586
|
+
useChatStore.setState({ activeTopicId: 'topic1' });
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// Should detect topic1 operation again
|
|
590
|
+
expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(true);
|
|
591
|
+
|
|
592
|
+
// Complete topic1 operation
|
|
593
|
+
act(() => {
|
|
594
|
+
result.current.completeOperation(topic1OpId!);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Should not detect any operation now
|
|
598
|
+
expect(operationSelectors.isMainWindowAgentRuntimeRunning(result.current)).toBe(false);
|
|
599
|
+
});
|
|
512
600
|
});
|
|
513
601
|
});
|
|
@@ -180,14 +180,27 @@ const isAgentRuntimeRunning = (s: ChatStoreState): boolean => {
|
|
|
180
180
|
/**
|
|
181
181
|
* Check if agent runtime is running in main window only
|
|
182
182
|
* Used for main window UI state (e.g., send button loading)
|
|
183
|
-
* Excludes thread operations to prevent cross-contamination
|
|
183
|
+
* Excludes thread operations and operations from other topics to prevent cross-contamination
|
|
184
184
|
*/
|
|
185
185
|
const isMainWindowAgentRuntimeRunning = (s: ChatStoreState): boolean => {
|
|
186
186
|
const operationIds = s.operationsByType['execAgentRuntime'] || [];
|
|
187
|
+
|
|
187
188
|
return operationIds.some((id) => {
|
|
188
189
|
const op = s.operations[id];
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
if (!op || op.status !== 'running' || op.metadata.isAborting || op.metadata.inThread) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Session must match
|
|
195
|
+
if (s.activeId !== op.context.sessionId) return false;
|
|
196
|
+
|
|
197
|
+
// Topic comparison: normalize null/undefined (both mean "default topic")
|
|
198
|
+
// activeTopicId can be null (initial state) or undefined (after topic operations)
|
|
199
|
+
// Operation context topicId can also be null or undefined
|
|
200
|
+
const activeTopicId = s.activeTopicId ?? null;
|
|
201
|
+
const opTopicId = op.context.topicId ?? null;
|
|
202
|
+
|
|
203
|
+
return activeTopicId === opTopicId;
|
|
191
204
|
});
|
|
192
205
|
};
|
|
193
206
|
|
|
@@ -32,6 +32,7 @@ export interface PluginOptimisticUpdateAction {
|
|
|
32
32
|
id: string,
|
|
33
33
|
value: T,
|
|
34
34
|
replace?: boolean,
|
|
35
|
+
context?: OptimisticUpdateContext,
|
|
35
36
|
) => Promise<void>;
|
|
36
37
|
|
|
37
38
|
/**
|
|
@@ -106,7 +107,7 @@ export const pluginOptimisticUpdate: StateCreator<
|
|
|
106
107
|
}
|
|
107
108
|
},
|
|
108
109
|
|
|
109
|
-
optimisticUpdatePluginArguments: async (id, value, replace = false) => {
|
|
110
|
+
optimisticUpdatePluginArguments: async (id, value, replace = false, context) => {
|
|
110
111
|
const { refreshMessages } = get();
|
|
111
112
|
const toolMessage = displayMessageSelectors.getDisplayMessageById(id)(get());
|
|
112
113
|
if (!toolMessage || !toolMessage?.tool_call_id) return;
|
|
@@ -121,20 +122,22 @@ export const pluginOptimisticUpdate: StateCreator<
|
|
|
121
122
|
if (isEqual(prevJson, nextValue)) return;
|
|
122
123
|
|
|
123
124
|
// optimistic update
|
|
124
|
-
get().internal_dispatchMessage(
|
|
125
|
-
id,
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
});
|
|
125
|
+
get().internal_dispatchMessage(
|
|
126
|
+
{ id, type: 'updateMessagePlugin', value: { arguments: JSON.stringify(nextValue) } },
|
|
127
|
+
context,
|
|
128
|
+
);
|
|
129
129
|
|
|
130
130
|
// 同样需要更新 assistantMessage 的 pluginArguments
|
|
131
131
|
if (assistantMessage) {
|
|
132
|
-
get().internal_dispatchMessage(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
132
|
+
get().internal_dispatchMessage(
|
|
133
|
+
{
|
|
134
|
+
id: assistantMessage.id,
|
|
135
|
+
type: 'updateMessageTools',
|
|
136
|
+
tool_call_id: toolMessage?.tool_call_id,
|
|
137
|
+
value: { arguments: JSON.stringify(nextValue) },
|
|
138
|
+
},
|
|
139
|
+
context,
|
|
140
|
+
);
|
|
138
141
|
assistantMessage = displayMessageSelectors.getDisplayMessageById(assistantMessage?.id)(get());
|
|
139
142
|
}
|
|
140
143
|
|
|
@@ -183,11 +186,14 @@ export const pluginOptimisticUpdate: StateCreator<
|
|
|
183
186
|
if (!assistantMessage) return;
|
|
184
187
|
|
|
185
188
|
const { internal_dispatchMessage, internal_refreshToUpdateMessageTools } = get();
|
|
186
|
-
internal_dispatchMessage(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
internal_dispatchMessage(
|
|
190
|
+
{
|
|
191
|
+
type: 'addMessageTool',
|
|
192
|
+
value: tool,
|
|
193
|
+
id: assistantMessage.id,
|
|
194
|
+
},
|
|
195
|
+
context,
|
|
196
|
+
);
|
|
191
197
|
|
|
192
198
|
await internal_refreshToUpdateMessageTools(id, context);
|
|
193
199
|
},
|
|
@@ -199,7 +205,7 @@ export const pluginOptimisticUpdate: StateCreator<
|
|
|
199
205
|
const { internal_dispatchMessage, internal_refreshToUpdateMessageTools } = get();
|
|
200
206
|
|
|
201
207
|
// optimistic update
|
|
202
|
-
internal_dispatchMessage({ type: 'deleteMessageTool', tool_call_id, id: message.id });
|
|
208
|
+
internal_dispatchMessage({ type: 'deleteMessageTool', tool_call_id, id: message.id }, context);
|
|
203
209
|
|
|
204
210
|
// update the message tools
|
|
205
211
|
await internal_refreshToUpdateMessageTools(id, context);
|
|
@@ -10,12 +10,7 @@ const getVaultByProvider = (provider: string) => (s: UserStore) =>
|
|
|
10
10
|
// @ts-ignore
|
|
11
11
|
(keyVaultsSettings(s)[provider] || {}) as any;
|
|
12
12
|
|
|
13
|
-
const password = (s: UserStore) => keyVaultsSettings(s).password || '';
|
|
14
|
-
|
|
15
13
|
export const keyVaultsConfigSelectors = {
|
|
16
14
|
getVaultByProvider,
|
|
17
|
-
|
|
18
15
|
keyVaultsSettings,
|
|
19
|
-
|
|
20
|
-
password,
|
|
21
16
|
};
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { Button, InputPassword } from '@lobehub/ui';
|
|
2
|
-
import { memo } from 'react';
|
|
3
|
-
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import { Flexbox } from 'react-layout-kit';
|
|
5
|
-
|
|
6
|
-
import { useChatStore } from '@/store/chat';
|
|
7
|
-
import { useUserStore } from '@/store/user';
|
|
8
|
-
import { keyVaultsConfigSelectors } from '@/store/user/selectors';
|
|
9
|
-
|
|
10
|
-
import { FormAction } from './style';
|
|
11
|
-
|
|
12
|
-
interface AccessCodeFormProps {
|
|
13
|
-
id: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const AccessCodeForm = memo<AccessCodeFormProps>(({ id }) => {
|
|
17
|
-
const { t } = useTranslation('error');
|
|
18
|
-
const [password, updateKeyVaults] = useUserStore((s) => [
|
|
19
|
-
keyVaultsConfigSelectors.password(s),
|
|
20
|
-
s.updateKeyVaults,
|
|
21
|
-
]);
|
|
22
|
-
const [resend, deleteMessage] = useChatStore((s) => [s.delAndRegenerateMessage, s.deleteMessage]);
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<>
|
|
26
|
-
<FormAction
|
|
27
|
-
avatar={'🗳'}
|
|
28
|
-
description={t('unlock.password.description')}
|
|
29
|
-
title={t('unlock.password.title')}
|
|
30
|
-
>
|
|
31
|
-
<InputPassword
|
|
32
|
-
autoComplete={'new-password'}
|
|
33
|
-
onChange={(e) => {
|
|
34
|
-
updateKeyVaults({ password: e.target.value });
|
|
35
|
-
}}
|
|
36
|
-
placeholder={t('unlock.password.placeholder')}
|
|
37
|
-
value={password}
|
|
38
|
-
variant={'filled'}
|
|
39
|
-
/>
|
|
40
|
-
</FormAction>
|
|
41
|
-
<Flexbox gap={12}>
|
|
42
|
-
<Button
|
|
43
|
-
onClick={() => {
|
|
44
|
-
resend(id);
|
|
45
|
-
deleteMessage(id);
|
|
46
|
-
}}
|
|
47
|
-
type={'primary'}
|
|
48
|
-
>
|
|
49
|
-
{t('unlock.confirm')}
|
|
50
|
-
</Button>
|
|
51
|
-
<Button
|
|
52
|
-
onClick={() => {
|
|
53
|
-
deleteMessage(id);
|
|
54
|
-
}}
|
|
55
|
-
>
|
|
56
|
-
{t('unlock.closeMessage')}
|
|
57
|
-
</Button>
|
|
58
|
-
</Flexbox>
|
|
59
|
-
</>
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
export default AccessCodeForm;
|