@lobehub/lobehub 2.0.0-next.302 → 2.0.0-next.303

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.303](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.302...v2.0.0-next.303)
6
+
7
+ <sup>Released on **2026-01-18**</sup>
8
+
9
+ #### 💄 Styles
10
+
11
+ - **misc**: Improve operation hint and fix scroll issue.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Styles
19
+
20
+ - **misc**: Improve operation hint and fix scroll issue, closes [#11573](https://github.com/lobehub/lobe-chat/issues/11573) ([8505d14](https://github.com/lobehub/lobe-chat/commit/8505d14))
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.302](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.301...v2.0.0-next.302)
6
31
 
7
32
  <sup>Released on **2026-01-17**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Improve operation hint and fix scroll issue."
6
+ ]
7
+ },
8
+ "date": "2026-01-18",
9
+ "version": "2.0.0-next.303"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "fixes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.302",
3
+ "version": "2.0.0-next.303",
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",
@@ -58,7 +58,7 @@ const AssistantMessage = memo<AssistantMessageProps>(
58
58
  const editing = useConversationStore(messageStateSelectors.isMessageEditing(id));
59
59
  const generating = useConversationStore(messageStateSelectors.isMessageGenerating(id));
60
60
  const creating = useConversationStore(messageStateSelectors.isMessageCreating(id));
61
- const newScreen = useNewScreen({ creating, isLatestItem });
61
+ const newScreen = useNewScreen({ creating: creating || generating, isLatestItem });
62
62
 
63
63
  const errorContent = useErrorContent(error);
64
64
 
@@ -1,9 +1,9 @@
1
1
  import { createStaticStyles, cx } from 'antd-style';
2
2
  import { memo } from 'react';
3
3
 
4
- import BubblesLoading from '@/components/BubblesLoading';
5
4
  import { LOADING_FLAT } from '@/const/message';
6
5
  import MarkdownMessage from '@/features/Conversation/Markdown';
6
+ import ContentLoading from '@/features/Conversation/Messages/components/ContentLoading';
7
7
 
8
8
  import { normalizeThinkTags, processWithArtifact } from '../../../utils/markdown';
9
9
  import { useMarkdown } from '../useMarkdown';
@@ -25,12 +25,12 @@ const MessageContent = memo<ContentBlockProps>(({ content, hasTools, id }) => {
25
25
  const message = normalizeThinkTags(processWithArtifact(content));
26
26
  const markdownProps = useMarkdown(id);
27
27
 
28
- if (!content && !hasTools) return <BubblesLoading />;
28
+ if (!content && !hasTools) return <ContentLoading id={id} />;
29
29
 
30
30
  if (content === LOADING_FLAT) {
31
31
  if (hasTools) return null;
32
32
 
33
- return <BubblesLoading />;
33
+ return <ContentLoading id={id} />;
34
34
  }
35
35
 
36
36
  return (
@@ -1,11 +1,11 @@
1
1
  import { memo } from 'react';
2
2
 
3
- import BubblesLoading from '@/components/BubblesLoading';
4
3
  import { LOADING_FLAT } from '@/const/message';
5
4
  import MarkdownMessage from '@/features/Conversation/Markdown';
6
5
 
7
6
  import { normalizeThinkTags, processWithArtifact } from '../../../utils/markdown';
8
7
  import { useMarkdown } from '../../AssistantGroup/useMarkdown';
8
+ import ContentLoading from '../../components/ContentLoading';
9
9
 
10
10
  interface ContentBlockProps {
11
11
  content: string;
@@ -20,7 +20,7 @@ const MessageContent = memo<ContentBlockProps>(({ content, id, hasTools }) => {
20
20
  if (!content || content === LOADING_FLAT) {
21
21
  if (hasTools) return null;
22
22
 
23
- return <BubblesLoading />;
23
+ return <ContentLoading id={id} />;
24
24
  }
25
25
 
26
26
  return content && <MarkdownMessage {...markdownProps}>{message}</MarkdownMessage>;
@@ -9,17 +9,17 @@ import type { OperationType } from '@/store/chat/slices/operation/types';
9
9
 
10
10
  const ELAPSED_TIME_THRESHOLD = 2100; // Show elapsed time after 2 seconds
11
11
 
12
+ const NO_NEED_SHOW_DOT_OP_TYPES = new Set<OperationType>(['reasoning']);
13
+
12
14
  interface ContentLoadingProps {
13
15
  id: string;
14
16
  }
15
17
 
16
18
  const ContentLoading = memo<ContentLoadingProps>(({ id }) => {
17
19
  const { t } = useTranslation('chat');
18
- const operations = useChatStore(operationSelectors.getOperationsByMessage(id));
20
+ const runningOp = useChatStore(operationSelectors.getDeepestRunningOperationByMessage(id));
19
21
  const [elapsedSeconds, setElapsedSeconds] = useState(0);
20
22
 
21
- // Get the running operation
22
- const runningOp = operations.find((op) => op.status === 'running');
23
23
  const operationType = runningOp?.type as OperationType | undefined;
24
24
  const startTime = runningOp?.metadata?.startTime;
25
25
 
@@ -48,6 +48,8 @@ const ContentLoading = memo<ContentLoadingProps>(({ id }) => {
48
48
 
49
49
  const showElapsedTime = elapsedSeconds >= ELAPSED_TIME_THRESHOLD / 1000;
50
50
 
51
+ if (operationType && NO_NEED_SHOW_DOT_OP_TYPES.has(operationType)) return null;
52
+
51
53
  return (
52
54
  <Flexbox align={'center'} horizontal>
53
55
  <BubblesLoading />
@@ -188,6 +188,171 @@ describe('Operation Selectors', () => {
188
188
  });
189
189
  });
190
190
 
191
+ describe('getDeepestRunningOperationByMessage', () => {
192
+ it('should return undefined when no operations exist', () => {
193
+ const { result } = renderHook(() => useChatStore());
194
+
195
+ const deepestOp = operationSelectors.getDeepestRunningOperationByMessage('msg1')(
196
+ result.current,
197
+ );
198
+
199
+ expect(deepestOp).toBeUndefined();
200
+ });
201
+
202
+ it('should return undefined when no running operations exist', () => {
203
+ const { result } = renderHook(() => useChatStore());
204
+
205
+ let opId: string;
206
+ act(() => {
207
+ opId = result.current.startOperation({
208
+ type: 'execAgentRuntime',
209
+ context: { agentId: 'session1', messageId: 'msg1' },
210
+ }).operationId;
211
+ result.current.associateMessageWithOperation('msg1', opId);
212
+ result.current.completeOperation(opId);
213
+ });
214
+
215
+ const deepestOp = operationSelectors.getDeepestRunningOperationByMessage('msg1')(
216
+ result.current,
217
+ );
218
+
219
+ expect(deepestOp).toBeUndefined();
220
+ });
221
+
222
+ it('should return the only running operation when there is one', () => {
223
+ const { result } = renderHook(() => useChatStore());
224
+
225
+ let opId: string;
226
+ act(() => {
227
+ opId = result.current.startOperation({
228
+ type: 'execAgentRuntime',
229
+ context: { agentId: 'session1', messageId: 'msg1' },
230
+ }).operationId;
231
+ result.current.associateMessageWithOperation('msg1', opId);
232
+ });
233
+
234
+ const deepestOp = operationSelectors.getDeepestRunningOperationByMessage('msg1')(
235
+ result.current,
236
+ );
237
+
238
+ expect(deepestOp).toBeDefined();
239
+ expect(deepestOp?.type).toBe('execAgentRuntime');
240
+ });
241
+
242
+ it('should return the leaf operation in a parent-child tree', () => {
243
+ const { result } = renderHook(() => useChatStore());
244
+
245
+ let parentOpId: string;
246
+ let childOpId: string;
247
+
248
+ act(() => {
249
+ // Start parent operation
250
+ parentOpId = result.current.startOperation({
251
+ type: 'execAgentRuntime',
252
+ context: { agentId: 'session1', messageId: 'msg1' },
253
+ }).operationId;
254
+ result.current.associateMessageWithOperation('msg1', parentOpId);
255
+
256
+ // Start child operation
257
+ childOpId = result.current.startOperation({
258
+ type: 'reasoning',
259
+ context: { agentId: 'session1', messageId: 'msg1' },
260
+ parentOperationId: parentOpId,
261
+ }).operationId;
262
+ result.current.associateMessageWithOperation('msg1', childOpId);
263
+ });
264
+
265
+ const deepestOp = operationSelectors.getDeepestRunningOperationByMessage('msg1')(
266
+ result.current,
267
+ );
268
+
269
+ // Should return the child (reasoning) not the parent (execAgentRuntime)
270
+ expect(deepestOp).toBeDefined();
271
+ expect(deepestOp?.type).toBe('reasoning');
272
+ expect(deepestOp?.id).toBe(childOpId!);
273
+ });
274
+
275
+ it('should return the deepest leaf in a multi-level tree', () => {
276
+ const { result } = renderHook(() => useChatStore());
277
+
278
+ let rootOpId: string;
279
+ let level1OpId: string;
280
+ let level2OpId: string;
281
+
282
+ act(() => {
283
+ // Level 0: root operation
284
+ rootOpId = result.current.startOperation({
285
+ type: 'execAgentRuntime',
286
+ context: { agentId: 'session1', messageId: 'msg1' },
287
+ }).operationId;
288
+ result.current.associateMessageWithOperation('msg1', rootOpId);
289
+
290
+ // Level 1: child of root
291
+ level1OpId = result.current.startOperation({
292
+ type: 'callLLM',
293
+ context: { agentId: 'session1', messageId: 'msg1' },
294
+ parentOperationId: rootOpId,
295
+ }).operationId;
296
+ result.current.associateMessageWithOperation('msg1', level1OpId);
297
+
298
+ // Level 2: grandchild (deepest)
299
+ level2OpId = result.current.startOperation({
300
+ type: 'reasoning',
301
+ context: { agentId: 'session1', messageId: 'msg1' },
302
+ parentOperationId: level1OpId,
303
+ }).operationId;
304
+ result.current.associateMessageWithOperation('msg1', level2OpId);
305
+ });
306
+
307
+ const deepestOp = operationSelectors.getDeepestRunningOperationByMessage('msg1')(
308
+ result.current,
309
+ );
310
+
311
+ // Should return the deepest leaf (reasoning at level 2)
312
+ expect(deepestOp).toBeDefined();
313
+ expect(deepestOp?.type).toBe('reasoning');
314
+ expect(deepestOp?.id).toBe(level2OpId!);
315
+ });
316
+
317
+ it('should return parent when child operation completes', () => {
318
+ const { result } = renderHook(() => useChatStore());
319
+
320
+ let parentOpId: string;
321
+ let childOpId: string;
322
+
323
+ act(() => {
324
+ parentOpId = result.current.startOperation({
325
+ type: 'execAgentRuntime',
326
+ context: { agentId: 'session1', messageId: 'msg1' },
327
+ }).operationId;
328
+ result.current.associateMessageWithOperation('msg1', parentOpId);
329
+
330
+ childOpId = result.current.startOperation({
331
+ type: 'reasoning',
332
+ context: { agentId: 'session1', messageId: 'msg1' },
333
+ parentOperationId: parentOpId,
334
+ }).operationId;
335
+ result.current.associateMessageWithOperation('msg1', childOpId);
336
+ });
337
+
338
+ // Before completing child
339
+ let deepestOp = operationSelectors.getDeepestRunningOperationByMessage('msg1')(
340
+ result.current,
341
+ );
342
+ expect(deepestOp?.type).toBe('reasoning');
343
+
344
+ // Complete child operation
345
+ act(() => {
346
+ result.current.completeOperation(childOpId);
347
+ });
348
+
349
+ // After completing child, parent should be the deepest running
350
+ deepestOp = operationSelectors.getDeepestRunningOperationByMessage('msg1')(result.current);
351
+ expect(deepestOp?.type).toBe('execAgentRuntime');
352
+ expect(deepestOp?.id).toBe(parentOpId!);
353
+ });
354
+ });
355
+
191
356
  describe('isMessageProcessing', () => {
192
357
  it('should return true if message has running operations', () => {
193
358
  const { result } = renderHook(() => useChatStore());
@@ -355,6 +355,28 @@ const isAnyMessageLoading =
355
355
  return messageIds.some((id) => isMessageProcessing(id)(s));
356
356
  };
357
357
 
358
+ /**
359
+ * Get the deepest running operation for a message (leaf node in operation tree)
360
+ * Operations form a tree structure via parentOperationId/childOperationIds
361
+ * This returns the most specific (deepest) running operation for UI display
362
+ */
363
+ const getDeepestRunningOperationByMessage =
364
+ (messageId: string) =>
365
+ (s: ChatStoreState): Operation | undefined => {
366
+ const operations = getOperationsByMessage(messageId)(s);
367
+ const runningOps = operations.filter((op) => op.status === 'running');
368
+
369
+ if (runningOps.length === 0) return undefined;
370
+
371
+ const runningOpIds = new Set(runningOps.map((op) => op.id));
372
+
373
+ // A leaf running operation has no running children
374
+ return runningOps.find((op) => {
375
+ const childIds = op.childOperationIds || [];
376
+ return !childIds.some((childId) => runningOpIds.has(childId));
377
+ });
378
+ };
379
+
358
380
  /**
359
381
  * Check if a specific message is being regenerated
360
382
  */
@@ -440,6 +462,7 @@ export const operationSelectors = {
440
462
  getCurrentContextOperations,
441
463
  getCurrentOperationLabel,
442
464
  getCurrentOperationProgress,
465
+ getDeepestRunningOperationByMessage,
443
466
  getOperationById,
444
467
  getOperationContextFromMessage,
445
468
  getOperationsByContext,