@lobehub/lobehub 2.0.0-next.8 → 2.0.0-next.9

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.
Files changed (109) hide show
  1. package/.github/workflows/desktop-pr-build.yml +8 -8
  2. package/.github/workflows/docker.yml +17 -16
  3. package/.github/workflows/e2e.yml +3 -3
  4. package/.github/workflows/release-desktop-beta.yml +8 -8
  5. package/.github/workflows/release.yml +1 -1
  6. package/.github/workflows/test.yml +4 -4
  7. package/CHANGELOG.md +25 -0
  8. package/changelog/v1.json +9 -0
  9. package/package.json +1 -1
  10. package/packages/const/src/index.ts +0 -1
  11. package/packages/const/src/url.ts +1 -4
  12. package/packages/context-engine/src/index.ts +1 -6
  13. package/packages/context-engine/src/processors/GroupMessageFlatten.ts +12 -2
  14. package/packages/context-engine/src/processors/__tests__/GroupMessageFlatten.test.ts +73 -9
  15. package/packages/context-engine/src/providers/index.ts +0 -2
  16. package/packages/database/package.json +1 -1
  17. package/packages/database/src/models/__tests__/message.grouping.test.ts +812 -0
  18. package/packages/database/src/models/__tests__/message.test.ts +322 -170
  19. package/packages/database/src/models/message.ts +62 -24
  20. package/packages/database/src/utils/__tests__/groupMessages.test.ts +145 -2
  21. package/packages/database/src/utils/groupMessages.ts +7 -5
  22. package/packages/types/src/message/common/base.ts +13 -0
  23. package/packages/types/src/message/common/image.ts +8 -0
  24. package/packages/types/src/message/common/metadata.ts +39 -0
  25. package/packages/types/src/message/common/tools.ts +10 -0
  26. package/packages/types/src/message/db/params.ts +47 -1
  27. package/packages/types/src/message/ui/chat.ts +4 -1
  28. package/packages/types/src/search.ts +16 -0
  29. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/V1Mobile/index.tsx +2 -2
  30. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/V1Mobile/useSend.ts +6 -4
  31. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/useSend.ts +15 -10
  32. package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/index.tsx +4 -2
  33. package/src/components/Thinking/index.tsx +4 -3
  34. package/src/features/AgentSetting/AgentPlugin/index.tsx +2 -2
  35. package/src/features/ChatInput/ActionBar/STT/browser.tsx +2 -2
  36. package/src/features/ChatInput/ActionBar/STT/openai.tsx +2 -2
  37. package/src/features/ChatInput/ActionBar/Tools/useControls.tsx +1 -3
  38. package/src/features/Conversation/Error/ErrorJsonViewer.tsx +4 -3
  39. package/src/features/Conversation/Error/OllamaBizError/index.tsx +7 -2
  40. package/src/features/Conversation/Error/index.tsx +15 -5
  41. package/src/features/Conversation/MarkdownElements/LobeArtifact/Render/index.tsx +2 -2
  42. package/src/features/Conversation/Messages/Assistant/Extra/index.tsx +2 -2
  43. package/src/features/Conversation/Messages/Assistant/MessageContent.tsx +5 -3
  44. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/BuiltinPluginTitle.tsx +2 -2
  45. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/ToolTitle.tsx +4 -2
  46. package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +2 -2
  47. package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +2 -2
  48. package/src/features/Conversation/Messages/Assistant/Tool/index.tsx +2 -2
  49. package/src/features/Conversation/Messages/Assistant/index.tsx +4 -4
  50. package/src/features/Conversation/Messages/Default.tsx +2 -2
  51. package/src/features/Conversation/Messages/User/Extra.tsx +2 -2
  52. package/src/features/Conversation/Messages/User/index.tsx +4 -4
  53. package/src/features/Conversation/Messages/index.tsx +3 -3
  54. package/src/features/Conversation/components/AutoScroll.tsx +2 -2
  55. package/src/features/Conversation/components/Extras/Usage/UsageDetail/index.tsx +9 -6
  56. package/src/features/PluginTag/index.tsx +1 -3
  57. package/src/features/PluginsUI/Render/BuiltinType/index.test.tsx +37 -28
  58. package/src/features/Portal/Artifacts/Body/index.tsx +2 -2
  59. package/src/server/modules/ModelRuntime/trace.ts +11 -4
  60. package/src/server/routers/lambda/message.ts +14 -3
  61. package/src/services/chat/chat.test.ts +1 -40
  62. package/src/services/chat/contextEngineering.test.ts +0 -30
  63. package/src/services/chat/contextEngineering.ts +1 -12
  64. package/src/services/chat/index.ts +2 -7
  65. package/src/services/chat/types.ts +1 -1
  66. package/src/services/message/_deprecated.ts +1 -1
  67. package/src/services/message/client.ts +8 -2
  68. package/src/services/message/server.ts +7 -2
  69. package/src/services/message/type.ts +6 -1
  70. package/src/store/chat/helpers.test.ts +99 -0
  71. package/src/store/chat/helpers.ts +21 -2
  72. package/src/store/chat/selectors.ts +1 -1
  73. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +3 -3
  74. package/src/store/chat/slices/builtinTool/actions/index.ts +1 -4
  75. package/src/store/chat/slices/message/action.test.ts +5 -1
  76. package/src/store/chat/slices/message/action.ts +102 -14
  77. package/src/store/chat/slices/message/reducer.test.ts +363 -5
  78. package/src/store/chat/slices/message/reducer.ts +87 -3
  79. package/src/store/chat/slices/message/{selectors.test.ts → selectors/chat.test.ts} +266 -30
  80. package/src/store/chat/slices/message/{selectors.ts → selectors/chat.ts} +29 -79
  81. package/src/store/chat/slices/message/selectors/index.ts +2 -0
  82. package/src/store/chat/slices/message/selectors/messageState.test.ts +36 -0
  83. package/src/store/chat/slices/message/selectors/messageState.ts +80 -0
  84. package/src/store/chat/slices/plugin/action.test.ts +34 -132
  85. package/src/store/chat/slices/plugin/action.ts +1 -44
  86. package/src/store/tool/selectors/tool.test.ts +1 -1
  87. package/src/store/tool/selectors/tool.ts +6 -8
  88. package/src/store/tool/slices/builtin/action.test.ts +83 -35
  89. package/src/store/tool/slices/builtin/action.ts +0 -9
  90. package/src/store/tool/slices/builtin/selectors.test.ts +4 -30
  91. package/src/store/tool/slices/builtin/selectors.ts +15 -21
  92. package/src/tools/index.ts +0 -6
  93. package/src/tools/renders.ts +0 -3
  94. package/src/tools/web-browsing/Portal/Search/Footer.tsx +2 -2
  95. package/packages/const/src/guide.ts +0 -89
  96. package/packages/context-engine/src/providers/InboxGuide.ts +0 -102
  97. package/packages/context-engine/src/providers/__tests__/InboxGuideProvider.test.ts +0 -121
  98. package/src/services/chat/__snapshots__/chat.test.ts.snap +0 -110
  99. package/src/store/chat/slices/builtinTool/actions/__tests__/dalle.test.ts +0 -121
  100. package/src/store/chat/slices/builtinTool/actions/dalle.ts +0 -124
  101. package/src/tools/dalle/Render/GalleyGrid.tsx +0 -60
  102. package/src/tools/dalle/Render/Item/EditMode.tsx +0 -66
  103. package/src/tools/dalle/Render/Item/Error.tsx +0 -49
  104. package/src/tools/dalle/Render/Item/Image.tsx +0 -44
  105. package/src/tools/dalle/Render/Item/ImageFileItem.tsx +0 -57
  106. package/src/tools/dalle/Render/Item/index.tsx +0 -88
  107. package/src/tools/dalle/Render/ToolBar.tsx +0 -56
  108. package/src/tools/dalle/Render/index.tsx +0 -52
  109. package/src/tools/dalle/index.ts +0 -92
@@ -75,6 +75,29 @@ interface UpdateMessageExtra {
75
75
  value: any;
76
76
  }
77
77
 
78
+ interface UpdateGroupBlockToolResult {
79
+ blockId: string;
80
+ groupMessageId: string;
81
+ toolId: string;
82
+ toolResult: {
83
+ content: string;
84
+ error?: any;
85
+ id: string;
86
+ state?: any;
87
+ };
88
+ type: 'updateGroupBlockToolResult';
89
+ }
90
+
91
+ interface AddGroupBlock {
92
+ blockId: string;
93
+ groupMessageId: string;
94
+ type: 'addGroupBlock';
95
+ value: {
96
+ content: string;
97
+ id: string;
98
+ };
99
+ }
100
+
78
101
  export type MessageDispatch =
79
102
  | CreateMessage
80
103
  | UpdateMessage
@@ -86,7 +109,9 @@ export type MessageDispatch =
86
109
  | UpdateMessageTools
87
110
  | AddMessageTool
88
111
  | DeleteMessageTool
89
- | DeleteMessages;
112
+ | DeleteMessages
113
+ | UpdateGroupBlockToolResult
114
+ | AddGroupBlock;
90
115
 
91
116
  export const messagesReducer = (
92
117
  state: UIChatMessage[],
@@ -96,10 +121,27 @@ export const messagesReducer = (
96
121
  case 'updateMessage': {
97
122
  return produce(state, (draftState) => {
98
123
  const { id, value } = payload;
124
+
125
+ // First, try to find in top-level messages
99
126
  const index = draftState.findIndex((i) => i.id === id);
100
- if (index < 0) return;
127
+ if (index >= 0) {
128
+ draftState[index] = merge(draftState[index], { ...value, updatedAt: Date.now() });
129
+ return;
130
+ }
101
131
 
102
- draftState[index] = merge(draftState[index], { ...value, updatedAt: Date.now() });
132
+ // If not found, search in group message children (blocks)
133
+ for (const message of draftState) {
134
+ if (message.role === 'group' && message.children) {
135
+ const blockIndex = message.children.findIndex((block) => block.id === id);
136
+ if (blockIndex >= 0) {
137
+ message.children[blockIndex] = merge(message.children[blockIndex], {
138
+ ...value,
139
+ });
140
+ message.updatedAt = Date.now();
141
+ return;
142
+ }
143
+ }
144
+ }
103
145
  });
104
146
  }
105
147
 
@@ -226,6 +268,48 @@ export const messagesReducer = (
226
268
  });
227
269
  });
228
270
  }
271
+
272
+ case 'updateGroupBlockToolResult': {
273
+ return produce(state, (draftState) => {
274
+ const { groupMessageId, blockId, toolId, toolResult } = payload;
275
+
276
+ // Find the group message
277
+ const msg = draftState.find((m) => m.id === groupMessageId);
278
+ if (!msg || msg.role !== 'group' || !msg.children) return;
279
+
280
+ // Find the block within children
281
+ const block = msg.children.find((b) => b.id === blockId);
282
+ if (!block || !block.tools) return;
283
+
284
+ // Find the tool and update its result
285
+ const tool = block.tools.find((t) => t.id === toolId);
286
+ if (!tool) return;
287
+
288
+ // Update tool result (optimistic update)
289
+ tool.result = toolResult;
290
+
291
+ msg.updatedAt = Date.now();
292
+ });
293
+ }
294
+
295
+ case 'addGroupBlock': {
296
+ return produce(state, (draftState) => {
297
+ const { groupMessageId, blockId, value } = payload;
298
+
299
+ // Find the group message
300
+ const msg = draftState.find((m) => m.id === groupMessageId);
301
+ if (!msg || msg.role !== 'group' || !msg.children) return;
302
+
303
+ // Add new block to children
304
+ msg.children.push({
305
+ content: value.content,
306
+ id: blockId,
307
+ });
308
+
309
+ msg.updatedAt = Date.now();
310
+ });
311
+ }
312
+
229
313
  default: {
230
314
  throw new Error('暂未实现的 type,请检查 reducer');
231
315
  }
@@ -12,7 +12,7 @@ import { messageMapKey } from '@/store/chat/utils/messageMapKey';
12
12
  import { createServerConfigStore } from '@/store/serverConfig/store';
13
13
  import { merge } from '@/utils/merge';
14
14
 
15
- import { chatSelectors } from './selectors';
15
+ import { chatSelectors } from './chat';
16
16
 
17
17
  vi.mock('i18next', () => ({
18
18
  t: vi.fn((key) => key), // Simplified mock return value
@@ -416,35 +416,6 @@ describe('chatSelectors', () => {
416
416
  });
417
417
  });
418
418
 
419
- describe('isToolCallStreaming', () => {
420
- it('should return true when tool call is streaming for given message and index', () => {
421
- const state: Partial<ChatStore> = {
422
- toolCallingStreamIds: {
423
- 'msg-1': [true, false, true],
424
- },
425
- };
426
- expect(chatSelectors.isToolCallStreaming('msg-1', 0)(state as ChatStore)).toBe(true);
427
- expect(chatSelectors.isToolCallStreaming('msg-1', 2)(state as ChatStore)).toBe(true);
428
- });
429
-
430
- it('should return false when tool call is not streaming for given message and index', () => {
431
- const state: Partial<ChatStore> = {
432
- toolCallingStreamIds: {
433
- 'msg-1': [true, false, true],
434
- },
435
- };
436
- expect(chatSelectors.isToolCallStreaming('msg-1', 1)(state as ChatStore)).toBe(false);
437
- expect(chatSelectors.isToolCallStreaming('msg-2', 0)(state as ChatStore)).toBe(false);
438
- });
439
-
440
- it('should return false when no streaming data exists for the message', () => {
441
- const state: Partial<ChatStore> = {
442
- toolCallingStreamIds: {},
443
- };
444
- expect(chatSelectors.isToolCallStreaming('msg-1', 0)(state as ChatStore)).toBe(false);
445
- });
446
- });
447
-
448
419
  describe('activeBaseChats with group chat messages', () => {
449
420
  it('should retrieve agent meta for group chat messages with groupId and agentId', () => {
450
421
  const groupChatMessages = [
@@ -470,4 +441,269 @@ describe('chatSelectors', () => {
470
441
  expect(chats[0].meta).toBeDefined();
471
442
  });
472
443
  });
444
+
445
+ describe('getGroupLatestMessageWithoutTools', () => {
446
+ it('should return the last child without tools', () => {
447
+ const groupMessage = {
448
+ id: 'group-1',
449
+ role: 'group',
450
+ content: '',
451
+ children: [
452
+ {
453
+ id: 'child-1',
454
+ content: 'First response',
455
+ tools: [
456
+ {
457
+ id: 'tool-1',
458
+ identifier: 'test',
459
+ apiName: 'test',
460
+ arguments: '{}',
461
+ type: 'default',
462
+ },
463
+ ],
464
+ },
465
+ {
466
+ id: 'child-2',
467
+ content: 'Second response',
468
+ tools: [],
469
+ },
470
+ {
471
+ id: 'child-3',
472
+ content: 'Final response',
473
+ },
474
+ ],
475
+ } as UIChatMessage;
476
+
477
+ const state: Partial<ChatStore> = {
478
+ activeId: 'test-id',
479
+ messagesMap: {
480
+ [messageMapKey('test-id')]: [groupMessage],
481
+ },
482
+ };
483
+
484
+ const result = chatSelectors.getGroupLatestMessageWithoutTools('group-1')(state as ChatStore);
485
+ expect(result).toBeDefined();
486
+ expect(result?.id).toBe('child-3');
487
+ expect(result?.content).toBe('Final response');
488
+ });
489
+
490
+ it('should return null if the last child has tools', () => {
491
+ const groupMessage = {
492
+ id: 'group-2',
493
+ role: 'group',
494
+ content: '',
495
+ children: [
496
+ {
497
+ id: 'child-1',
498
+ content: 'First response',
499
+ },
500
+ {
501
+ id: 'child-2',
502
+ content: 'Second response with tools',
503
+ tools: [
504
+ {
505
+ id: 'tool-1',
506
+ identifier: 'test',
507
+ apiName: 'test',
508
+ arguments: '{}',
509
+ type: 'default',
510
+ },
511
+ ],
512
+ },
513
+ ],
514
+ } as UIChatMessage;
515
+
516
+ const state: Partial<ChatStore> = {
517
+ activeId: 'test-id',
518
+ messagesMap: {
519
+ [messageMapKey('test-id')]: [groupMessage],
520
+ },
521
+ };
522
+
523
+ const result = chatSelectors.getGroupLatestMessageWithoutTools('group-2')(state as ChatStore);
524
+ expect(result).toBeUndefined();
525
+ });
526
+
527
+ it('should return the last child when it has empty tools array', () => {
528
+ const groupMessage = {
529
+ id: 'group-3',
530
+ role: 'group',
531
+ content: '',
532
+ children: [
533
+ {
534
+ id: 'child-1',
535
+ content: 'First response with tools',
536
+ tools: [
537
+ {
538
+ id: 'tool-1',
539
+ identifier: 'test',
540
+ apiName: 'test',
541
+ arguments: '{}',
542
+ type: 'default',
543
+ },
544
+ ],
545
+ },
546
+ {
547
+ id: 'child-2',
548
+ content: 'Final response',
549
+ tools: [],
550
+ },
551
+ ],
552
+ } as UIChatMessage;
553
+
554
+ const state: Partial<ChatStore> = {
555
+ activeId: 'test-id',
556
+ messagesMap: {
557
+ [messageMapKey('test-id')]: [groupMessage],
558
+ },
559
+ };
560
+
561
+ const result = chatSelectors.getGroupLatestMessageWithoutTools('group-3')(state as ChatStore);
562
+ expect(result).toBeDefined();
563
+ expect(result?.id).toBe('child-2');
564
+ expect(result?.content).toBe('Final response');
565
+ });
566
+
567
+ it('should return null for non-group messages', () => {
568
+ const assistantMessage = {
569
+ id: 'msg-1',
570
+ role: 'assistant',
571
+ content: 'Regular message',
572
+ } as UIChatMessage;
573
+
574
+ const state: Partial<ChatStore> = {
575
+ activeId: 'test-id',
576
+ messagesMap: {
577
+ [messageMapKey('test-id')]: [assistantMessage],
578
+ },
579
+ };
580
+
581
+ const result = chatSelectors.getGroupLatestMessageWithoutTools('msg-1')(state as ChatStore);
582
+ expect(result).toBeUndefined();
583
+ });
584
+
585
+ it('should return null for group messages without children', () => {
586
+ const groupMessage = {
587
+ id: 'group-4',
588
+ role: 'group',
589
+ content: '',
590
+ children: undefined,
591
+ } as UIChatMessage;
592
+
593
+ const state: Partial<ChatStore> = {
594
+ activeId: 'test-id',
595
+ messagesMap: {
596
+ [messageMapKey('test-id')]: [groupMessage],
597
+ },
598
+ };
599
+
600
+ const result = chatSelectors.getGroupLatestMessageWithoutTools('group-4')(state as ChatStore);
601
+ expect(result).toBeUndefined();
602
+ });
603
+
604
+ it('should return null for group messages with empty children array', () => {
605
+ const groupMessage = {
606
+ id: 'group-5',
607
+ role: 'group',
608
+ content: '',
609
+ children: [],
610
+ } as unknown as UIChatMessage;
611
+
612
+ const state: Partial<ChatStore> = {
613
+ activeId: 'test-id',
614
+ messagesMap: {
615
+ [messageMapKey('test-id')]: [groupMessage],
616
+ },
617
+ };
618
+
619
+ const result = chatSelectors.getGroupLatestMessageWithoutTools('group-5')(state as ChatStore);
620
+ expect(result).toBeUndefined();
621
+ });
622
+
623
+ it('should return null if all children have tools', () => {
624
+ const groupMessage = {
625
+ id: 'group-6',
626
+ role: 'group',
627
+ content: '',
628
+ children: [
629
+ {
630
+ id: 'child-1',
631
+ content: 'First response',
632
+ tools: [
633
+ {
634
+ id: 'tool-1',
635
+ identifier: 'test',
636
+ apiName: 'test',
637
+ arguments: '{}',
638
+ type: 'default',
639
+ },
640
+ ],
641
+ },
642
+ {
643
+ id: 'child-2',
644
+ content: 'Second response',
645
+ tools: [
646
+ {
647
+ id: 'tool-2',
648
+ identifier: 'test2',
649
+ apiName: 'test2',
650
+ arguments: '{}',
651
+ type: 'default',
652
+ },
653
+ ],
654
+ },
655
+ ],
656
+ } as unknown as UIChatMessage;
657
+
658
+ const state: Partial<ChatStore> = {
659
+ activeId: 'test-id',
660
+ messagesMap: {
661
+ [messageMapKey('test-id')]: [groupMessage],
662
+ },
663
+ };
664
+
665
+ const result = chatSelectors.getGroupLatestMessageWithoutTools('group-6')(state as ChatStore);
666
+ expect(result).toBeUndefined();
667
+ });
668
+
669
+ it('should handle empty tools array as no tools', () => {
670
+ const groupMessage = {
671
+ id: 'group-7',
672
+ role: 'group',
673
+ content: '',
674
+ children: [
675
+ {
676
+ id: 'child-1',
677
+ content: 'Response with empty tools',
678
+ tools: [],
679
+ },
680
+ ],
681
+ } as unknown as UIChatMessage;
682
+
683
+ const state: Partial<ChatStore> = {
684
+ activeId: 'test-id',
685
+ messagesMap: {
686
+ [messageMapKey('test-id')]: [groupMessage],
687
+ },
688
+ };
689
+
690
+ const result = chatSelectors.getGroupLatestMessageWithoutTools('group-7')(state as ChatStore);
691
+ expect(result).toBeDefined();
692
+ expect(result?.id).toBe('child-1');
693
+ });
694
+
695
+ it('should return null when message is not found', () => {
696
+ const state: Partial<ChatStore> = {
697
+ activeId: 'test-id',
698
+ messagesMap: {
699
+ [messageMapKey('test-id')]: [],
700
+ },
701
+ };
702
+
703
+ const result = chatSelectors.getGroupLatestMessageWithoutTools('non-existent')(
704
+ state as ChatStore,
705
+ );
706
+ expect(result).toBeUndefined();
707
+ });
708
+ });
473
709
  });
@@ -10,8 +10,8 @@ import { sessionMetaSelectors } from '@/store/session/selectors';
10
10
  import { useUserStore } from '@/store/user';
11
11
  import { userProfileSelectors } from '@/store/user/selectors';
12
12
 
13
- import { chatHelpers } from '../../helpers';
14
- import type { ChatStoreState } from '../../initialState';
13
+ import { chatHelpers } from '../../../helpers';
14
+ import type { ChatStoreState } from '../../../initialState';
15
15
 
16
16
  const getMeta = (message: UIChatMessage) => {
17
17
  switch (message.role) {
@@ -47,7 +47,7 @@ const getBaseChatsByKey =
47
47
  return messages.map((i) => ({ ...i, meta: getMeta(i) }));
48
48
  };
49
49
 
50
- const currentChatKey = (s: ChatStoreState) => messageMapKey(s.activeId, s.activeTopicId);
50
+ export const currentChatKey = (s: ChatStoreState) => messageMapKey(s.activeId, s.activeTopicId);
51
51
 
52
52
  /**
53
53
  * Current active raw message list, include thread messages
@@ -89,7 +89,7 @@ const mainDisplayChats = (s: ChatStoreState): UIChatMessage[] => {
89
89
  return getChatsWithThread(s, displayChats);
90
90
  };
91
91
 
92
- const mainDisplayChatIDs = (s: ChatStoreState) => mainDisplayChats(s).map((s) => s.id);
92
+ export const mainDisplayChatIDs = (s: ChatStoreState) => mainDisplayChats(s).map((s) => s.id);
93
93
 
94
94
  const mainAIChats = (s: ChatStoreState): UIChatMessage[] => {
95
95
  const messages = activeBaseChats(s);
@@ -154,7 +154,7 @@ const countMessagesByThreadId = (id: string) => (s: ChatStoreState) => {
154
154
  return messages.length;
155
155
  };
156
156
 
157
- const getMessageByToolCallId = (id: string) => (s: ChatStoreState) => {
157
+ export const getMessageByToolCallId = (id: string) => (s: ChatStoreState) => {
158
158
  const messages = activeBaseChats(s);
159
159
  return messages.find((m) => m.tool_call_id === id);
160
160
  };
@@ -166,67 +166,6 @@ const currentChatLoadingState = (s: ChatStoreState) => !s.messagesInit;
166
166
 
167
167
  const isCurrentChatLoaded = (s: ChatStoreState) => !!s.messagesMap[currentChatKey(s)];
168
168
 
169
- const isMessageEditing = (id: string) => (s: ChatStoreState) => s.messageEditingIds.includes(id);
170
- const isMessageLoading = (id: string) => (s: ChatStoreState) => s.messageLoadingIds.includes(id);
171
-
172
- const isMessageGenerating = (id: string) => (s: ChatStoreState) => s.chatLoadingIds.includes(id);
173
- const isMessageInRAGFlow = (id: string) => (s: ChatStoreState) =>
174
- s.messageRAGLoadingIds.includes(id);
175
- const isMessageInChatReasoning = (id: string) => (s: ChatStoreState) =>
176
- s.reasoningLoadingIds.includes(id);
177
-
178
- const isPluginApiInvoking = (id: string) => (s: ChatStoreState) =>
179
- s.pluginApiLoadingIds.includes(id);
180
-
181
- const isToolCallStreaming = (id: string, index: number) => (s: ChatStoreState) => {
182
- const isLoading = s.toolCallingStreamIds[id];
183
-
184
- if (!isLoading) return false;
185
-
186
- return isLoading[index];
187
- };
188
-
189
- const isInToolsCalling = (id: string, index: number) => (s: ChatStoreState) => {
190
- const isStreamingToolsCalling = isToolCallStreaming(id, index)(s);
191
-
192
- const isInvokingPluginApi = s.messageInToolsCallingIds.includes(id);
193
-
194
- return isStreamingToolsCalling || isInvokingPluginApi;
195
- };
196
-
197
- const isToolApiNameShining =
198
- (messageId: string, index: number, toolCallId: string) => (s: ChatStoreState) => {
199
- const toolMessageId = getMessageByToolCallId(toolCallId)(s)?.id;
200
- const isStreaming = isToolCallStreaming(messageId, index)(s);
201
- const isPluginInvoking = !toolMessageId ? true : isPluginApiInvoking(toolMessageId)(s);
202
-
203
- return isStreaming || isPluginInvoking;
204
- };
205
-
206
- const isAIGenerating = (s: ChatStoreState) =>
207
- s.chatLoadingIds.some((id) => mainDisplayChatIDs(s).includes(id));
208
-
209
- const isInRAGFlow = (s: ChatStoreState) =>
210
- s.messageRAGLoadingIds.some((id) => mainDisplayChatIDs(s).includes(id));
211
-
212
- const isCreatingMessage = (s: ChatStoreState) => s.isCreatingMessage;
213
-
214
- const isHasMessageLoading = (s: ChatStoreState) =>
215
- s.messageLoadingIds.some((id) => mainDisplayChatIDs(s).includes(id));
216
-
217
- /**
218
- * this function is used to determine whether the send button should be disabled
219
- */
220
- const isSendButtonDisabledByMessage = (s: ChatStoreState) =>
221
- // 1. when there is message loading
222
- isHasMessageLoading(s) ||
223
- // 2. when is creating the topic
224
- s.creatingTopic ||
225
- // 3. when is creating the message
226
- isCreatingMessage(s) ||
227
- // 4. when the message is in RAG flow
228
- isInRAGFlow(s);
229
-
230
169
  const inboxActiveTopicMessages = (state: ChatStoreState) => {
231
170
  const activeTopicId = state.activeTopicId;
232
171
  return state.messagesMap[messageMapKey(INBOX_SESSION_ID, activeTopicId)] || [];
@@ -280,6 +219,29 @@ const getSupervisorTodos = (groupId?: string, topicId?: string | null) => (s: Ch
280
219
  return s.supervisorTodos[messageMapKey(groupId, topicId)] || [];
281
220
  };
282
221
 
222
+ /**
223
+ * Gets the latest message block from a group message that doesn't contain tools
224
+ * Returns null if the last block contains tools or if message is not a group message
225
+ */
226
+ const getGroupLatestMessageWithoutTools = (id: string) => (s: ChatStoreState) => {
227
+ const message = getMessageById(id)(s);
228
+
229
+ if (!message || message.role !== 'group' || !message.children || message.children.length === 0)
230
+ return;
231
+
232
+ // Get the last child
233
+ const lastChild = message.children.at(-1);
234
+
235
+ if (!lastChild) return;
236
+
237
+ // Return the last child only if it doesn't have tools
238
+ if (!lastChild.tools || lastChild.tools.length === 0) {
239
+ return lastChild;
240
+ }
241
+
242
+ return;
243
+ };
244
+
283
245
  export const chatSelectors = {
284
246
  activeBaseChats,
285
247
  activeBaseChatsWithoutTool,
@@ -289,6 +251,7 @@ export const chatSelectors = {
289
251
  currentToolMessages,
290
252
  currentUserFiles,
291
253
  getBaseChatsByKey,
254
+ getGroupLatestMessageWithoutTools,
292
255
  getMessageById,
293
256
  getMessageByToolCallId,
294
257
  getSupervisorTodos,
@@ -296,21 +259,8 @@ export const chatSelectors = {
296
259
  getThreadMessages,
297
260
  getTraceIdByMessageId,
298
261
  inboxActiveTopicMessages,
299
- isAIGenerating,
300
- isCreatingMessage,
301
262
  isCurrentChatLoaded,
302
- isHasMessageLoading,
303
- isInToolsCalling,
304
- isMessageEditing,
305
- isMessageGenerating,
306
- isMessageInChatReasoning,
307
- isMessageInRAGFlow,
308
- isMessageLoading,
309
- isPluginApiInvoking,
310
- isSendButtonDisabledByMessage,
311
263
  isSupervisorLoading,
312
- isToolApiNameShining,
313
- isToolCallStreaming,
314
264
  latestMessage,
315
265
  mainAIChats,
316
266
  mainAIChatsMessageString,
@@ -0,0 +1,2 @@
1
+ export * from './chat';
2
+ export * from './messageState';
@@ -0,0 +1,36 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { ChatStore } from '@/store/chat';
4
+
5
+ import { messageStateSelectors } from './messageState';
6
+
7
+ describe('messageStateSelectors', () => {
8
+ describe('isToolCallStreaming', () => {
9
+ it('should return true when tool call is streaming for given message and index', () => {
10
+ const state: Partial<ChatStore> = {
11
+ toolCallingStreamIds: {
12
+ 'msg-1': [true, false, true],
13
+ },
14
+ };
15
+ expect(messageStateSelectors.isToolCallStreaming('msg-1', 0)(state as ChatStore)).toBe(true);
16
+ expect(messageStateSelectors.isToolCallStreaming('msg-1', 2)(state as ChatStore)).toBe(true);
17
+ });
18
+
19
+ it('should return false when tool call is not streaming for given message and index', () => {
20
+ const state: Partial<ChatStore> = {
21
+ toolCallingStreamIds: {
22
+ 'msg-1': [true, false, true],
23
+ },
24
+ };
25
+ expect(messageStateSelectors.isToolCallStreaming('msg-1', 1)(state as ChatStore)).toBe(false);
26
+ expect(messageStateSelectors.isToolCallStreaming('msg-2', 0)(state as ChatStore)).toBe(false);
27
+ });
28
+
29
+ it('should return false when no streaming data exists for the message', () => {
30
+ const state: Partial<ChatStore> = {
31
+ toolCallingStreamIds: {},
32
+ };
33
+ expect(messageStateSelectors.isToolCallStreaming('msg-1', 0)(state as ChatStore)).toBe(false);
34
+ });
35
+ });
36
+ });