@lobehub/lobehub 2.0.0-next.358 → 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 CHANGED
@@ -2,6 +2,31 @@
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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ## [Version 2.0.0-next.358](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.357...v2.0.0-next.358)
6
31
 
7
32
  <sup>Released on **2026-01-23**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
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
+ },
2
11
  {
3
12
  "children": {},
4
13
  "date": "2026-01-23",
@@ -361,8 +361,10 @@ Then('Agent 应该从列表中移除', async function (this: CustomWorld) {
361
361
 
362
362
  await this.page.waitForTimeout(500);
363
363
 
364
- if (this.testContext.targetItemId) {
365
- const deletedItem = this.page.locator(`a[aria-label="${this.testContext.targetItemId}"]`);
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.358",
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
- const { done, value } = await it.next();
155
- if (done) controller.close();
156
- else controller.enqueue(value);
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
- const { done, value } = await it.next();
175
- if (done) controller.close();
176
- else controller.enqueue(value);
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 { type RefObject, memo, useCallback, useEffect, useRef } from 'react';
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
- interface CronJobContentEditorInnerProps extends CronJobContentEditorProps {
24
- contentRef: RefObject<string>;
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, contentRef],
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 MutableRefObject, type ReactNode, memo, useRef } from 'react';
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
- interface ChatInputProviderInnerProps extends StoreUpdaterProps {
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, contentRef] =
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={(e) => {
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;