@lobehub/lobehub 2.0.0-next.357 β 2.0.0-next.359
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 +50 -0
- package/changelog/v1.json +14 -0
- package/e2e/src/steps/home/sidebarAgent.steps.ts +4 -2
- package/package.json +1 -1
- package/packages/model-runtime/src/core/streams/protocol.test.ts +29 -0
- package/packages/model-runtime/src/core/streams/protocol.ts +26 -6
- package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobContentEditor.tsx +21 -34
- package/src/features/ChatInput/ChatInputProvider.tsx +2 -22
- package/src/features/ChatInput/InputEditor/index.tsx +3 -14
- package/src/features/ChatInput/store/initialState.ts +0 -2
- package/src/features/Conversation/store/slices/generation/action.test.ts +116 -0
- package/src/features/Conversation/store/slices/generation/action.ts +14 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.359](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.358...v2.0.0-next.359)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2026-01-24**</sup>
|
|
8
|
+
|
|
9
|
+
#### π Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Surface streaming errors during mid-stream pulls.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Surface streaming errors during mid-stream pulls, closes [#11762](https://github.com/lobehub/lobe-chat/issues/11762) ([74a88d3](https://github.com/lobehub/lobe-chat/commit/74a88d3))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
## [Version 2.0.0-next.358](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.357...v2.0.0-next.358)
|
|
31
|
+
|
|
32
|
+
<sup>Released on **2026-01-23**</sup>
|
|
33
|
+
|
|
34
|
+
#### π Bug Fixes
|
|
35
|
+
|
|
36
|
+
- **store**: Delete message before regeneration.
|
|
37
|
+
|
|
38
|
+
<br/>
|
|
39
|
+
|
|
40
|
+
<details>
|
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
42
|
+
|
|
43
|
+
#### What's fixed
|
|
44
|
+
|
|
45
|
+
- **store**: Delete message before regeneration, closes [#11760](https://github.com/lobehub/lobe-chat/issues/11760) ([a8a6300](https://github.com/lobehub/lobe-chat/commit/a8a6300))
|
|
46
|
+
|
|
47
|
+
</details>
|
|
48
|
+
|
|
49
|
+
<div align="right">
|
|
50
|
+
|
|
51
|
+
[](#readme-top)
|
|
52
|
+
|
|
53
|
+
</div>
|
|
54
|
+
|
|
5
55
|
## [Version 2.0.0-next.357](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.356...v2.0.0-next.357)
|
|
6
56
|
|
|
7
57
|
<sup>Released on **2026-01-23**</sup>
|
package/changelog/v1.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"children": {
|
|
4
|
+
"fixes": [
|
|
5
|
+
"Surface streaming errors during mid-stream pulls."
|
|
6
|
+
]
|
|
7
|
+
},
|
|
8
|
+
"date": "2026-01-24",
|
|
9
|
+
"version": "2.0.0-next.359"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"children": {},
|
|
13
|
+
"date": "2026-01-23",
|
|
14
|
+
"version": "2.0.0-next.358"
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"children": {
|
|
4
18
|
"fixes": [
|
|
@@ -361,8 +361,10 @@ Then('Agent εΊθ―₯δ»ε葨δΈη§»ι€', async function (this: CustomWorld) {
|
|
|
361
361
|
|
|
362
362
|
await this.page.waitForTimeout(500);
|
|
363
363
|
|
|
364
|
-
|
|
365
|
-
|
|
364
|
+
// Use unique selector based on agent ID (href) to avoid false positives
|
|
365
|
+
// when multiple agents have the same name
|
|
366
|
+
if (this.testContext.targetItemSelector) {
|
|
367
|
+
const deletedItem = this.page.locator(this.testContext.targetItemSelector);
|
|
366
368
|
await expect(deletedItem).not.toBeVisible({ timeout: 5000 });
|
|
367
369
|
}
|
|
368
370
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.359",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
FIRST_CHUNK_ERROR_KEY,
|
|
5
|
+
convertIterableToStream,
|
|
4
6
|
createCallbacksTransformer,
|
|
7
|
+
createFirstErrorHandleTransformer,
|
|
5
8
|
createSSEDataExtractor,
|
|
6
9
|
createSSEProtocolTransformer,
|
|
7
10
|
createTokenSpeedCalculator,
|
|
@@ -239,6 +242,32 @@ describe('createTokenSpeedCalculator', async () => {
|
|
|
239
242
|
});
|
|
240
243
|
});
|
|
241
244
|
|
|
245
|
+
describe('convertIterableToStream', () => {
|
|
246
|
+
it('should surface errors from subsequent pulls as error chunks', async () => {
|
|
247
|
+
async function* erroringStream() {
|
|
248
|
+
yield 'first';
|
|
249
|
+
throw new Error('rate limit');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const readable = convertIterableToStream(erroringStream()).pipeThrough(
|
|
253
|
+
createFirstErrorHandleTransformer(),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const reader = readable.getReader();
|
|
257
|
+
const chunks: any[] = [];
|
|
258
|
+
|
|
259
|
+
while (true) {
|
|
260
|
+
const { done, value } = await reader.read();
|
|
261
|
+
if (done) break;
|
|
262
|
+
chunks.push(value);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
expect(chunks[0]).toBe('first');
|
|
266
|
+
expect(chunks[1][FIRST_CHUNK_ERROR_KEY]).toBe(true);
|
|
267
|
+
expect(chunks[1].message).toBe('rate limit');
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
242
271
|
describe('createSSEProtocolTransformer', () => {
|
|
243
272
|
const processChunk = async (transformer: TransformStream, chunk: any) => {
|
|
244
273
|
const results: any[] = [];
|
|
@@ -151,9 +151,19 @@ export function readableFromAsyncIterable<T>(iterable: AsyncIterable<T>) {
|
|
|
151
151
|
},
|
|
152
152
|
|
|
153
153
|
async pull(controller) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
try {
|
|
155
|
+
const { done, value } = await it.next();
|
|
156
|
+
if (done) controller.close();
|
|
157
|
+
else controller.enqueue(value);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
const error = e as Error;
|
|
160
|
+
|
|
161
|
+
controller.enqueue(
|
|
162
|
+
(ERROR_CHUNK_PREFIX +
|
|
163
|
+
JSON.stringify({ message: error.message, name: error.name, stack: error.stack })) as T,
|
|
164
|
+
);
|
|
165
|
+
controller.close();
|
|
166
|
+
}
|
|
157
167
|
},
|
|
158
168
|
});
|
|
159
169
|
}
|
|
@@ -171,9 +181,19 @@ export const convertIterableToStream = <T>(stream: AsyncIterable<T>) => {
|
|
|
171
181
|
await it.return?.(reason);
|
|
172
182
|
},
|
|
173
183
|
async pull(controller) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
184
|
+
try {
|
|
185
|
+
const { done, value } = await it.next();
|
|
186
|
+
if (done) controller.close();
|
|
187
|
+
else controller.enqueue(value);
|
|
188
|
+
} catch (e) {
|
|
189
|
+
const error = e as Error;
|
|
190
|
+
|
|
191
|
+
controller.enqueue(
|
|
192
|
+
(ERROR_CHUNK_PREFIX +
|
|
193
|
+
JSON.stringify({ message: error.message, name: error.name, stack: error.stack })) as T,
|
|
194
|
+
);
|
|
195
|
+
controller.close();
|
|
196
|
+
}
|
|
177
197
|
},
|
|
178
198
|
|
|
179
199
|
async start(controller) {
|
|
@@ -11,7 +11,7 @@ import { Editor, useEditor } from '@lobehub/editor/react';
|
|
|
11
11
|
import { Flexbox, Icon, Text } from '@lobehub/ui';
|
|
12
12
|
import { Card } from 'antd';
|
|
13
13
|
import { Clock } from 'lucide-react';
|
|
14
|
-
import {
|
|
14
|
+
import { memo, useCallback, useEffect, useRef } from 'react';
|
|
15
15
|
import { useTranslation } from 'react-i18next';
|
|
16
16
|
|
|
17
17
|
interface CronJobContentEditorProps {
|
|
@@ -20,12 +20,8 @@ interface CronJobContentEditorProps {
|
|
|
20
20
|
onChange: (value: string) => void;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const CronJobContentEditorInner = memo<CronJobContentEditorInnerProps>(
|
|
28
|
-
({ enableRichRender, initialValue, onChange, contentRef }) => {
|
|
23
|
+
const CronJobContentEditor = memo<CronJobContentEditorProps>(
|
|
24
|
+
({ enableRichRender, initialValue, onChange }) => {
|
|
29
25
|
const { t } = useTranslation('setting');
|
|
30
26
|
const editor = useEditor();
|
|
31
27
|
const currentValueRef = useRef(initialValue);
|
|
@@ -35,6 +31,23 @@ const CronJobContentEditorInner = memo<CronJobContentEditorInnerProps>(
|
|
|
35
31
|
currentValueRef.current = initialValue;
|
|
36
32
|
}, [initialValue]);
|
|
37
33
|
|
|
34
|
+
// Initialize editor content when editor is ready
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!editor) return;
|
|
37
|
+
try {
|
|
38
|
+
setTimeout(() => {
|
|
39
|
+
if (initialValue) {
|
|
40
|
+
editor.setDocument(enableRichRender ? 'markdown' : 'text', initialValue);
|
|
41
|
+
}
|
|
42
|
+
}, 100);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('[CronJobContentEditor] Failed to initialize editor content:', error);
|
|
45
|
+
setTimeout(() => {
|
|
46
|
+
editor.setDocument(enableRichRender ? 'markdown' : 'text', initialValue);
|
|
47
|
+
}, 100);
|
|
48
|
+
}
|
|
49
|
+
}, [editor, enableRichRender, initialValue]);
|
|
50
|
+
|
|
38
51
|
// Handle content changes
|
|
39
52
|
const handleContentChange = useCallback(
|
|
40
53
|
(e: any) => {
|
|
@@ -44,18 +57,13 @@ const CronJobContentEditorInner = memo<CronJobContentEditorInnerProps>(
|
|
|
44
57
|
|
|
45
58
|
const finalContent = nextContent || '';
|
|
46
59
|
|
|
47
|
-
// Save to parent ref for restoration
|
|
48
|
-
if (contentRef) {
|
|
49
|
-
(contentRef as { current: string }).current = finalContent;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
60
|
// Only call onChange if content actually changed
|
|
53
61
|
if (finalContent !== currentValueRef.current) {
|
|
54
62
|
currentValueRef.current = finalContent;
|
|
55
63
|
onChange(finalContent);
|
|
56
64
|
}
|
|
57
65
|
},
|
|
58
|
-
[enableRichRender, onChange
|
|
66
|
+
[enableRichRender, onChange],
|
|
59
67
|
);
|
|
60
68
|
|
|
61
69
|
return (
|
|
@@ -74,14 +82,6 @@ const CronJobContentEditorInner = memo<CronJobContentEditorInnerProps>(
|
|
|
74
82
|
content={''}
|
|
75
83
|
editor={editor}
|
|
76
84
|
lineEmptyPlaceholder={t('agentCronJobs.form.content.placeholder')}
|
|
77
|
-
onInit={(editor) => {
|
|
78
|
-
// Restore content from parent ref when editor re-initializes
|
|
79
|
-
if (contentRef?.current) {
|
|
80
|
-
editor.setDocument(enableRichRender ? 'markdown' : 'text', contentRef.current);
|
|
81
|
-
} else if (initialValue) {
|
|
82
|
-
editor.setDocument(enableRichRender ? 'markdown' : 'text', initialValue);
|
|
83
|
-
}
|
|
84
|
-
}}
|
|
85
85
|
onTextChange={handleContentChange}
|
|
86
86
|
placeholder={t('agentCronJobs.form.content.placeholder')}
|
|
87
87
|
plugins={
|
|
@@ -108,17 +108,4 @@ const CronJobContentEditorInner = memo<CronJobContentEditorInnerProps>(
|
|
|
108
108
|
},
|
|
109
109
|
);
|
|
110
110
|
|
|
111
|
-
const CronJobContentEditor = (props: CronJobContentEditorProps) => {
|
|
112
|
-
// Ref to persist content across re-mounts when enableRichRender changes
|
|
113
|
-
const contentRef = useRef<string>(props.initialValue);
|
|
114
|
-
|
|
115
|
-
return (
|
|
116
|
-
<CronJobContentEditorInner
|
|
117
|
-
contentRef={contentRef}
|
|
118
|
-
key={`editor-${props.enableRichRender}`}
|
|
119
|
-
{...props}
|
|
120
|
-
/>
|
|
121
|
-
);
|
|
122
|
-
};
|
|
123
|
-
|
|
124
111
|
export default CronJobContentEditor;
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { useEditor } from '@lobehub/editor/react';
|
|
2
|
-
import { type
|
|
3
|
-
|
|
4
|
-
import { useUserStore } from '@/store/user';
|
|
5
|
-
import { labPreferSelectors } from '@/store/user/selectors';
|
|
2
|
+
import { type ReactNode, memo, useRef } from 'react';
|
|
6
3
|
|
|
7
4
|
import StoreUpdater, { type StoreUpdaterProps } from './StoreUpdater';
|
|
8
5
|
import { Provider, createStore } from './store';
|
|
@@ -11,16 +8,10 @@ interface ChatInputProviderProps extends StoreUpdaterProps {
|
|
|
11
8
|
children: ReactNode;
|
|
12
9
|
}
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
children: ReactNode;
|
|
16
|
-
contentRef: MutableRefObject<string>;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const ChatInputProviderInner = memo<ChatInputProviderInnerProps>(
|
|
11
|
+
export const ChatInputProvider = memo<ChatInputProviderProps>(
|
|
20
12
|
({
|
|
21
13
|
agentId,
|
|
22
14
|
children,
|
|
23
|
-
contentRef,
|
|
24
15
|
leftActions,
|
|
25
16
|
rightActions,
|
|
26
17
|
mobile,
|
|
@@ -40,7 +31,6 @@ const ChatInputProviderInner = memo<ChatInputProviderInnerProps>(
|
|
|
40
31
|
createStore={() =>
|
|
41
32
|
createStore({
|
|
42
33
|
allowExpand,
|
|
43
|
-
contentRef,
|
|
44
34
|
editor,
|
|
45
35
|
leftActions,
|
|
46
36
|
mentionItems,
|
|
@@ -70,13 +60,3 @@ const ChatInputProviderInner = memo<ChatInputProviderInnerProps>(
|
|
|
70
60
|
);
|
|
71
61
|
},
|
|
72
62
|
);
|
|
73
|
-
|
|
74
|
-
export const ChatInputProvider = (props: ChatInputProviderProps) => {
|
|
75
|
-
const enableRichRender = useUserStore(labPreferSelectors.enableInputMarkdown);
|
|
76
|
-
// Ref to persist content across re-mounts when enableRichRender changes
|
|
77
|
-
const contentRef = useRef<string>('');
|
|
78
|
-
|
|
79
|
-
return (
|
|
80
|
-
<ChatInputProviderInner contentRef={contentRef} key={`editor-${enableRichRender}`} {...props} />
|
|
81
|
-
);
|
|
82
|
-
};
|
|
@@ -37,7 +37,7 @@ const className = cx(css`
|
|
|
37
37
|
`);
|
|
38
38
|
|
|
39
39
|
const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
|
|
40
|
-
const [editor, slashMenuRef, send, updateMarkdownContent, expand, mentionItems
|
|
40
|
+
const [editor, slashMenuRef, send, updateMarkdownContent, expand, mentionItems] =
|
|
41
41
|
useChatInputStore((s) => [
|
|
42
42
|
s.editor,
|
|
43
43
|
s.slashMenuRef,
|
|
@@ -45,7 +45,6 @@ const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
|
|
|
45
45
|
s.updateMarkdownContent,
|
|
46
46
|
s.expand,
|
|
47
47
|
s.mentionItems,
|
|
48
|
-
s.contentRef,
|
|
49
48
|
]);
|
|
50
49
|
|
|
51
50
|
const storeApi = useStoreApi();
|
|
@@ -152,11 +151,7 @@ const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
|
|
|
152
151
|
onBlur={() => {
|
|
153
152
|
disableScope(HotkeyEnum.AddUserMessage);
|
|
154
153
|
}}
|
|
155
|
-
onChange={(
|
|
156
|
-
// Save content to parent ref for restoration when enableRichRender changes
|
|
157
|
-
if (contentRef) {
|
|
158
|
-
contentRef.current = e.getDocument('markdown') as unknown as string;
|
|
159
|
-
}
|
|
154
|
+
onChange={() => {
|
|
160
155
|
updateMarkdownContent();
|
|
161
156
|
}}
|
|
162
157
|
onCompositionEnd={() => {
|
|
@@ -182,13 +177,7 @@ const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
|
|
|
182
177
|
onFocus={() => {
|
|
183
178
|
enableScope(HotkeyEnum.AddUserMessage);
|
|
184
179
|
}}
|
|
185
|
-
onInit={(editor) => {
|
|
186
|
-
storeApi.setState({ editor });
|
|
187
|
-
// Restore content from parent ref when editor re-initializes
|
|
188
|
-
if (contentRef?.current) {
|
|
189
|
-
editor.setDocument('markdown', contentRef.current);
|
|
190
|
-
}
|
|
191
|
-
}}
|
|
180
|
+
onInit={(editor) => storeApi.setState({ editor })}
|
|
192
181
|
onPressEnter={({ event: e }) => {
|
|
193
182
|
if (e.shiftKey || isChineseInput.current) return;
|
|
194
183
|
// when user like alt + enter to add ai message
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { type IEditor, type SlashOptions } from '@lobehub/editor';
|
|
2
2
|
import type { ChatInputProps } from '@lobehub/editor/react';
|
|
3
3
|
import type { MenuProps } from '@lobehub/ui';
|
|
4
|
-
import type { MutableRefObject } from 'react';
|
|
5
4
|
|
|
6
5
|
import { type ActionKeys } from '@/features/ChatInput';
|
|
7
6
|
|
|
@@ -40,7 +39,6 @@ export interface PublicState {
|
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
export interface State extends PublicState {
|
|
43
|
-
contentRef?: MutableRefObject<string>;
|
|
44
42
|
editor?: IEditor;
|
|
45
43
|
isContentEmpty: boolean;
|
|
46
44
|
markdownContent: string;
|
|
@@ -462,6 +462,122 @@ describe('Generation Actions', () => {
|
|
|
462
462
|
// Should complete operation
|
|
463
463
|
expect(mockCompleteOperation).toHaveBeenCalledWith('test-op-id');
|
|
464
464
|
});
|
|
465
|
+
|
|
466
|
+
it('should delete message BEFORE regeneration to prevent message not found issue (LOBE-2533)', async () => {
|
|
467
|
+
// This test verifies the fix for LOBE-2533:
|
|
468
|
+
// When "delete and regenerate" is called, if regeneration happens first,
|
|
469
|
+
// it switches to a new branch, causing the original message to no longer
|
|
470
|
+
// appear in displayMessages. Then deleteMessage cannot find the message
|
|
471
|
+
// and fails silently.
|
|
472
|
+
//
|
|
473
|
+
// The fix: delete first, then regenerate.
|
|
474
|
+
|
|
475
|
+
const callOrder: string[] = [];
|
|
476
|
+
|
|
477
|
+
// Re-setup mock to track call order
|
|
478
|
+
const { useChatStore } = await import('@/store/chat');
|
|
479
|
+
vi.mocked(useChatStore.getState).mockReturnValue({
|
|
480
|
+
messagesMap: {},
|
|
481
|
+
operations: {},
|
|
482
|
+
messageLoadingIds: [],
|
|
483
|
+
cancelOperations: mockCancelOperations,
|
|
484
|
+
cancelOperation: mockCancelOperation,
|
|
485
|
+
deleteMessage: vi.fn().mockImplementation(() => {
|
|
486
|
+
callOrder.push('deleteMessage');
|
|
487
|
+
return Promise.resolve();
|
|
488
|
+
}),
|
|
489
|
+
switchMessageBranch: vi.fn().mockImplementation(() => {
|
|
490
|
+
callOrder.push('switchMessageBranch');
|
|
491
|
+
return Promise.resolve();
|
|
492
|
+
}),
|
|
493
|
+
startOperation: mockStartOperation,
|
|
494
|
+
completeOperation: mockCompleteOperation,
|
|
495
|
+
failOperation: mockFailOperation,
|
|
496
|
+
internal_execAgentRuntime: vi.fn().mockImplementation(() => {
|
|
497
|
+
callOrder.push('internal_execAgentRuntime');
|
|
498
|
+
return Promise.resolve();
|
|
499
|
+
}),
|
|
500
|
+
} as any);
|
|
501
|
+
|
|
502
|
+
const context: ConversationContext = {
|
|
503
|
+
agentId: 'session-1',
|
|
504
|
+
topicId: 'topic-1',
|
|
505
|
+
threadId: null,
|
|
506
|
+
groupId: 'group-1',
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const store = createStore({ context });
|
|
510
|
+
|
|
511
|
+
// Set displayMessages and dbMessages
|
|
512
|
+
act(() => {
|
|
513
|
+
store.setState({
|
|
514
|
+
displayMessages: [
|
|
515
|
+
{ id: 'msg-1', role: 'user', content: 'Hello' },
|
|
516
|
+
{ id: 'msg-2', role: 'assistant', content: 'Hi there', parentId: 'msg-1' },
|
|
517
|
+
],
|
|
518
|
+
dbMessages: [
|
|
519
|
+
{ id: 'msg-1', role: 'user', content: 'Hello' },
|
|
520
|
+
{ id: 'msg-2', role: 'assistant', content: 'Hi there', parentId: 'msg-1' },
|
|
521
|
+
],
|
|
522
|
+
} as any);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
await act(async () => {
|
|
526
|
+
await store.getState().delAndRegenerateMessage('msg-2');
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// CRITICAL: deleteMessage must be called BEFORE switchMessageBranch and internal_execAgentRuntime
|
|
530
|
+
// If regeneration (which calls switchMessageBranch) happens first, the message
|
|
531
|
+
// won't be found in displayMessages and deletion will fail silently.
|
|
532
|
+
expect(callOrder[0]).toBe('deleteMessage');
|
|
533
|
+
expect(callOrder).toContain('switchMessageBranch');
|
|
534
|
+
expect(callOrder).toContain('internal_execAgentRuntime');
|
|
535
|
+
|
|
536
|
+
// Verify deleteMessage is called before any regeneration-related calls
|
|
537
|
+
const deleteIndex = callOrder.indexOf('deleteMessage');
|
|
538
|
+
const switchIndex = callOrder.indexOf('switchMessageBranch');
|
|
539
|
+
const execIndex = callOrder.indexOf('internal_execAgentRuntime');
|
|
540
|
+
|
|
541
|
+
expect(deleteIndex).toBeLessThan(switchIndex);
|
|
542
|
+
expect(deleteIndex).toBeLessThan(execIndex);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('should not proceed if assistant message has no parentId', async () => {
|
|
546
|
+
const { useChatStore } = await import('@/store/chat');
|
|
547
|
+
vi.mocked(useChatStore.getState).mockReturnValue({
|
|
548
|
+
messagesMap: {},
|
|
549
|
+
operations: {},
|
|
550
|
+
messageLoadingIds: [],
|
|
551
|
+
startOperation: mockStartOperation,
|
|
552
|
+
completeOperation: mockCompleteOperation,
|
|
553
|
+
deleteMessage: mockDeleteMessage,
|
|
554
|
+
} as any);
|
|
555
|
+
|
|
556
|
+
const context: ConversationContext = {
|
|
557
|
+
agentId: 'session-1',
|
|
558
|
+
topicId: null,
|
|
559
|
+
threadId: null,
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const store = createStore({ context });
|
|
563
|
+
|
|
564
|
+
// Set displayMessages with assistant message that has no parentId
|
|
565
|
+
act(() => {
|
|
566
|
+
store.setState({
|
|
567
|
+
displayMessages: [
|
|
568
|
+
{ id: 'msg-1', role: 'assistant', content: 'Hi there' }, // no parentId
|
|
569
|
+
],
|
|
570
|
+
} as any);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
await act(async () => {
|
|
574
|
+
await store.getState().delAndRegenerateMessage('msg-1');
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// Should not proceed - no operation created, no delete called
|
|
578
|
+
expect(mockStartOperation).not.toHaveBeenCalled();
|
|
579
|
+
expect(mockDeleteMessage).not.toHaveBeenCalled();
|
|
580
|
+
});
|
|
465
581
|
});
|
|
466
582
|
|
|
467
583
|
describe('delAndResendThreadMessage', () => {
|
|
@@ -206,18 +206,29 @@ export const generationSlice: StateCreator<
|
|
|
206
206
|
},
|
|
207
207
|
|
|
208
208
|
delAndRegenerateMessage: async (messageId: string) => {
|
|
209
|
-
const { context } = get();
|
|
209
|
+
const { context, displayMessages } = get();
|
|
210
210
|
const chatStore = useChatStore.getState();
|
|
211
211
|
|
|
212
|
+
// Find the assistant message and get parent user message ID before deletion
|
|
213
|
+
// This is needed because after deletion, we can't find the parent anymore
|
|
214
|
+
const currentMessage = displayMessages.find((c) => c.id === messageId);
|
|
215
|
+
if (!currentMessage) return;
|
|
216
|
+
|
|
217
|
+
const userId = currentMessage.parentId;
|
|
218
|
+
if (!userId) return;
|
|
219
|
+
|
|
212
220
|
// Create operation to track context (use 'regenerate' type since this is a regenerate action)
|
|
213
221
|
const { operationId } = chatStore.startOperation({
|
|
214
222
|
context: { ...context, messageId },
|
|
215
223
|
type: 'regenerate',
|
|
216
224
|
});
|
|
217
225
|
|
|
218
|
-
//
|
|
219
|
-
|
|
226
|
+
// IMPORTANT: Delete first, then regenerate (LOBE-2533)
|
|
227
|
+
// If we regenerate first, it switches to a new branch, causing the original
|
|
228
|
+
// message to no longer appear in displayMessages. Then deleteMessage cannot
|
|
229
|
+
// find the message and fails silently.
|
|
220
230
|
await chatStore.deleteMessage(messageId, { operationId });
|
|
231
|
+
await get().regenerateUserMessage(userId);
|
|
221
232
|
chatStore.completeOperation(operationId);
|
|
222
233
|
},
|
|
223
234
|
|