@lobehub/lobehub 2.0.0-next.23 → 2.0.0-next.25

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 (82) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/labs.json +4 -0
  4. package/locales/bg-BG/labs.json +4 -0
  5. package/locales/de-DE/labs.json +4 -0
  6. package/locales/en-US/labs.json +4 -0
  7. package/locales/es-ES/labs.json +4 -0
  8. package/locales/fa-IR/labs.json +4 -0
  9. package/locales/fr-FR/labs.json +4 -0
  10. package/locales/it-IT/labs.json +4 -0
  11. package/locales/ja-JP/labs.json +4 -0
  12. package/locales/ko-KR/labs.json +4 -0
  13. package/locales/nl-NL/labs.json +4 -0
  14. package/locales/pl-PL/labs.json +4 -0
  15. package/locales/pt-BR/labs.json +4 -0
  16. package/locales/ru-RU/labs.json +4 -0
  17. package/locales/tr-TR/labs.json +4 -0
  18. package/locales/vi-VN/labs.json +4 -0
  19. package/locales/zh-CN/labs.json +4 -0
  20. package/locales/zh-TW/labs.json +4 -0
  21. package/package.json +1 -1
  22. package/packages/const/src/user.ts +5 -2
  23. package/packages/types/src/index.ts +0 -1
  24. package/packages/types/src/user/index.ts +2 -88
  25. package/packages/types/src/user/preference.ts +105 -0
  26. package/renovate.json +1 -6
  27. package/src/app/[variants]/(main)/labs/components/LabCard.tsx +5 -5
  28. package/src/app/[variants]/(main)/labs/page.tsx +19 -22
  29. package/src/app/[variants]/(main)/settings/provider/detail/azure/index.tsx +1 -1
  30. package/src/app/[variants]/(main)/settings/provider/detail/azureai/index.tsx +1 -1
  31. package/src/app/[variants]/(main)/settings/provider/detail/bedrock/index.tsx +1 -1
  32. package/src/app/[variants]/(main)/settings/provider/detail/cloudflare/index.tsx +1 -1
  33. package/src/app/[variants]/(main)/settings/provider/detail/comfyui/index.tsx +1 -1
  34. package/src/app/[variants]/(main)/settings/provider/detail/github/index.tsx +1 -1
  35. package/src/app/[variants]/(main)/settings/provider/detail/vertexai/index.tsx +1 -1
  36. package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/index.tsx +2 -4
  37. package/src/components/Skeleton/SkeletonSwitch.tsx +13 -0
  38. package/src/components/Skeleton/index.ts +2 -0
  39. package/src/features/ChatInput/ActionBar/index.tsx +2 -2
  40. package/src/features/ChatInput/InputEditor/index.tsx +2 -2
  41. package/src/features/Conversation/Messages/Group/Actions/WithContentId.tsx +152 -0
  42. package/src/features/Conversation/Messages/Group/Actions/WithoutContentId.tsx +70 -0
  43. package/src/features/Conversation/Messages/Group/Actions/index.tsx +21 -0
  44. package/src/features/Conversation/Messages/Group/ContentBlock.tsx +91 -0
  45. package/src/features/Conversation/Messages/Group/EditState.tsx +51 -0
  46. package/src/features/Conversation/Messages/Group/Error/index.tsx +53 -0
  47. package/src/features/Conversation/Messages/Group/GroupChildren.tsx +73 -0
  48. package/src/features/Conversation/Messages/Group/MessageContent.tsx +39 -0
  49. package/src/features/Conversation/Messages/Group/Tool/Inspector/BuiltinPluginTitle.tsx +49 -0
  50. package/src/features/Conversation/Messages/Group/Tool/Inspector/Debug.tsx +70 -0
  51. package/src/features/Conversation/Messages/Group/Tool/Inspector/PluginResult.tsx +34 -0
  52. package/src/features/Conversation/Messages/Group/Tool/Inspector/PluginState.tsx +18 -0
  53. package/src/features/Conversation/Messages/Group/Tool/Inspector/Settings.tsx +40 -0
  54. package/src/features/Conversation/Messages/Group/Tool/Inspector/ToolTitle.tsx +92 -0
  55. package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +176 -0
  56. package/src/features/Conversation/Messages/Group/Tool/Render/Arguments/ObjectEntity.tsx +81 -0
  57. package/src/features/Conversation/Messages/Group/Tool/Render/Arguments/ValueCell.tsx +43 -0
  58. package/src/features/Conversation/Messages/Group/Tool/Render/Arguments/index.tsx +134 -0
  59. package/src/features/Conversation/Messages/Group/Tool/Render/CustomRender.tsx +88 -0
  60. package/src/features/Conversation/Messages/Group/Tool/Render/ErrorResponse.tsx +35 -0
  61. package/src/features/Conversation/Messages/Group/Tool/Render/LoadingPlaceholder/index.tsx +29 -0
  62. package/src/features/Conversation/Messages/Group/Tool/Render/PluginSettings.tsx +66 -0
  63. package/src/features/Conversation/Messages/Group/Tool/Render/index.tsx +105 -0
  64. package/src/features/Conversation/Messages/Group/Tool/index.tsx +75 -0
  65. package/src/features/Conversation/Messages/Group/Tools.tsx +46 -0
  66. package/src/features/Conversation/Messages/Group/index.tsx +140 -0
  67. package/src/features/Conversation/Messages/index.tsx +12 -0
  68. package/src/features/Conversation/components/ShareMessageModal/ShareImage/Preview.tsx +2 -2
  69. package/src/locales/default/labs.ts +4 -0
  70. package/src/server/routers/lambda/message.ts +5 -20
  71. package/src/services/chat/contextEngineering.ts +6 -5
  72. package/src/services/message/server.ts +10 -10
  73. package/src/services/message/type.ts +0 -2
  74. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +309 -2
  75. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +2 -22
  76. package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +272 -14
  77. package/src/store/user/selectors.ts +1 -1
  78. package/src/store/user/slices/preference/action.ts +8 -1
  79. package/src/store/user/slices/preference/selectors/index.ts +2 -0
  80. package/src/store/user/slices/preference/selectors/labPrefer.ts +13 -0
  81. package/src/store/user/slices/preference/{selectors.ts → selectors/preference.ts} +0 -2
  82. /package/src/{app/[variants]/(main)/settings/provider/features/ProviderConfig → components/Skeleton}/SkeletonInput.tsx +0 -0
@@ -0,0 +1,140 @@
1
+ 'use client';
2
+
3
+ import { UIChatMessage } from '@lobechat/types';
4
+ import { useResponsive } from 'antd-style';
5
+ import { memo, useCallback } from 'react';
6
+ import { Flexbox } from 'react-layout-kit';
7
+
8
+ import Avatar from '@/features/ChatItem/components/Avatar';
9
+ import BorderSpacing from '@/features/ChatItem/components/BorderSpacing';
10
+ import Title from '@/features/ChatItem/components/Title';
11
+ import { useStyles } from '@/features/ChatItem/style';
12
+ import GroupChildren from '@/features/Conversation/Messages/Group/GroupChildren';
13
+ import Usage from '@/features/Conversation/components/Extras/Usage';
14
+ import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes';
15
+ import { useAgentStore } from '@/store/agent';
16
+ import { agentChatConfigSelectors } from '@/store/agent/selectors';
17
+ import { useChatStore } from '@/store/chat';
18
+ import { chatSelectors, messageStateSelectors } from '@/store/chat/slices/message/selectors';
19
+ import { useGlobalStore } from '@/store/global';
20
+ import { useSessionStore } from '@/store/session';
21
+ import { sessionSelectors } from '@/store/session/selectors';
22
+
23
+ import { GroupActionsBar } from './Actions';
24
+ import EditState from './EditState';
25
+
26
+ const MOBILE_AVATAR_SIZE = 32;
27
+
28
+ interface GroupMessageProps extends UIChatMessage {
29
+ disableEditing?: boolean;
30
+ index: number;
31
+ showTitle?: boolean;
32
+ }
33
+
34
+ const GroupMessage = memo<GroupMessageProps>((props) => {
35
+ const {
36
+ showTitle,
37
+ id,
38
+ disableEditing,
39
+ usage,
40
+ index,
41
+ createdAt,
42
+ meta,
43
+ children,
44
+ performance,
45
+ model,
46
+ provider,
47
+ } = props;
48
+ const avatar = meta;
49
+ const { mobile } = useResponsive();
50
+ const placement = 'left';
51
+ const type = useAgentStore(agentChatConfigSelectors.displayMode);
52
+ const variant = type === 'chat' ? 'bubble' : 'docs';
53
+
54
+ const { styles } = useStyles({
55
+ editing: false,
56
+ placement,
57
+ primary: false,
58
+ showTitle,
59
+ time: createdAt,
60
+ title: avatar.title,
61
+ variant,
62
+ });
63
+
64
+ const [isInbox] = useSessionStore((s) => [sessionSelectors.isInboxSession(s)]);
65
+ const [toggleSystemRole] = useGlobalStore((s) => [s.toggleSystemRole]);
66
+ const openChatSettings = useOpenChatSettings();
67
+ const lastAssistantMsg = useChatStore(chatSelectors.getGroupLatestMessageWithoutTools(id));
68
+
69
+ const contentId = lastAssistantMsg?.id;
70
+
71
+ const isEditing = useChatStore(messageStateSelectors.isMessageEditing(contentId || ''));
72
+
73
+ // ======================= Performance Optimization ======================= //
74
+ // these useMemo/useCallback are all for the performance optimization
75
+ // maybe we can remove it in React 19
76
+ // ======================================================================== //
77
+ const onAvatarClick = useCallback(() => {
78
+ if (!isInbox) {
79
+ toggleSystemRole(true);
80
+ } else {
81
+ openChatSettings();
82
+ }
83
+ }, [isInbox]);
84
+
85
+ return (
86
+ <Flexbox className={styles.container} gap={mobile ? 6 : 12}>
87
+ <Flexbox gap={4} horizontal>
88
+ <Avatar
89
+ alt={avatar.title || 'avatar'}
90
+ avatar={avatar}
91
+ onClick={onAvatarClick}
92
+ placement={placement}
93
+ size={mobile ? MOBILE_AVATAR_SIZE : undefined}
94
+ style={{ marginTop: 6 }}
95
+ />
96
+ <Title avatar={avatar} placement={placement} showTitle time={createdAt} />
97
+ </Flexbox>
98
+ {isEditing && contentId ? (
99
+ <EditState content={lastAssistantMsg?.content} id={contentId} />
100
+ ) : (
101
+ <Flexbox
102
+ align={'flex-start'}
103
+ className={styles.messageContent}
104
+ data-layout={'vertical'}
105
+ direction={'vertical'}
106
+ gap={8}
107
+ width={'100%'}
108
+ >
109
+ {children && children.length > 0 && (
110
+ <GroupChildren
111
+ blocks={children}
112
+ contentId={contentId}
113
+ disableEditing={disableEditing}
114
+ messageIndex={index}
115
+ />
116
+ )}
117
+
118
+ {model && (
119
+ <Usage metadata={{ ...performance, ...usage }} model={model} provider={provider!} />
120
+ )}
121
+ {!disableEditing && (
122
+ <Flexbox align={'flex-start'} className={styles.actions} role="menubar">
123
+ <GroupActionsBar
124
+ contentBlock={lastAssistantMsg}
125
+ contentId={contentId}
126
+ data={props}
127
+ id={id}
128
+ index={index}
129
+ />
130
+ </Flexbox>
131
+ )}
132
+ </Flexbox>
133
+ )}
134
+
135
+ {mobile && <BorderSpacing borderSpacing={MOBILE_AVATAR_SIZE} />}
136
+ </Flexbox>
137
+ );
138
+ });
139
+
140
+ export default GroupMessage;
@@ -16,6 +16,7 @@ import { chatSelectors, messageStateSelectors } from '@/store/chat/selectors';
16
16
  import History from '../components/History';
17
17
  import { InPortalThreadContext } from '../context/InPortalThreadContext';
18
18
  import AssistantMessage from './Assistant';
19
+ import GroupMessage from './Group';
19
20
  import SupervisorMessage from './Supervisor';
20
21
  import UserMessage from './User';
21
22
 
@@ -132,6 +133,17 @@ const Item = memo<ChatListItemProps>(
132
133
  );
133
134
  }
134
135
 
136
+ case 'group': {
137
+ return (
138
+ <GroupMessage
139
+ {...item}
140
+ disableEditing={disableEditing}
141
+ index={index}
142
+ showTitle={item.groupId ? true : false}
143
+ />
144
+ );
145
+ }
146
+
135
147
  case 'supervisor': {
136
148
  return <SupervisorMessage {...item} disableEditing={disableEditing} index={index} />;
137
149
  }
@@ -1,3 +1,4 @@
1
+ import { OFFICIAL_DOMAIN } from '@lobechat/const';
1
2
  import { UIChatMessage } from '@lobechat/types';
2
3
  import { ModelTag } from '@lobehub/icons';
3
4
  import { Avatar } from '@lobehub/ui';
@@ -14,7 +15,6 @@ import { agentSelectors } from '@/store/agent/selectors';
14
15
  import { useSessionStore } from '@/store/session';
15
16
  import { sessionMetaSelectors, sessionSelectors } from '@/store/session/selectors';
16
17
 
17
- import pkg from '../../../../../../package.json';
18
18
  import { useContainerStyles } from '../style';
19
19
  import { useStyles } from './style';
20
20
  import { FieldType } from './type';
@@ -75,7 +75,7 @@ const Preview = memo<PreviewProps>(
75
75
  {withFooter ? (
76
76
  <Flexbox align={'center'} className={styles.footer} gap={4}>
77
77
  <ProductLogo type={'combine'} />
78
- <div className={styles.url}>{pkg.homepage}</div>
78
+ <div className={styles.url}>{OFFICIAL_DOMAIN}</div>
79
79
  </Flexbox>
80
80
  ) : (
81
81
  <div />
@@ -1,6 +1,10 @@
1
1
  export default {
2
2
  desc: '这里会不定期更新我们正在探索的新功能,欢迎试用!',
3
3
  features: {
4
+ assistantMessageGroup: {
5
+ desc: '将助手消息及其工具调用结果聚合到分组里显示',
6
+ title: '助手消息聚合分组',
7
+ },
4
8
  groupChat: {
5
9
  desc: '启用多智能体群聊编排能力。',
6
10
  title: '群聊(多智能体)',
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  CreateMessageParamsSchema,
3
3
  CreateNewMessageParamsSchema,
4
- UIChatMessage,
5
4
  UpdateMessageParamsSchema,
6
5
  UpdateMessageRAGParamsSchema,
7
6
  } from '@lobechat/types';
@@ -14,8 +13,6 @@ import { authedProcedure, publicProcedure, router } from '@/libs/trpc/lambda';
14
13
  import { serverDatabase } from '@/libs/trpc/lambda/middleware';
15
14
  import { FileService } from '@/server/services/file';
16
15
 
17
- type ChatMessageList = UIChatMessage[];
18
-
19
16
  const messageProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
20
17
  const { ctx } = opts;
21
18
 
@@ -72,22 +69,6 @@ export const messageRouter = router({
72
69
  });
73
70
  }),
74
71
 
75
- // TODO: it will be removed in V2
76
- getAllMessages: messageProcedure.query(async ({ ctx }): Promise<ChatMessageList> => {
77
- return ctx.messageModel.queryAll() as any;
78
- }),
79
-
80
- // TODO: it will be removed in V2
81
- getAllMessagesInSession: messageProcedure
82
- .input(
83
- z.object({
84
- sessionId: z.string().nullable().optional(),
85
- }),
86
- )
87
- .query(async ({ ctx, input }): Promise<ChatMessageList> => {
88
- return ctx.messageModel.queryBySessionId(input.sessionId) as any;
89
- }),
90
-
91
72
  getHeatmaps: messageProcedure.query(async ({ ctx }) => {
92
73
  return ctx.messageModel.getHeatmaps();
93
74
  }),
@@ -101,16 +82,20 @@ export const messageRouter = router({
101
82
  pageSize: z.number().optional(),
102
83
  sessionId: z.string().nullable().optional(),
103
84
  topicId: z.string().nullable().optional(),
85
+ useGroup: z.boolean().optional(),
104
86
  }),
105
87
  )
106
88
  .query(async ({ input, ctx }) => {
107
89
  if (!ctx.userId) return [];
108
90
  const serverDB = await getServerDB();
109
91
 
92
+ const { useGroup, ...queryParams } = input;
93
+
110
94
  const messageModel = new MessageModel(serverDB, ctx.userId);
111
95
  const fileService = new FileService(serverDB, ctx.userId);
112
96
 
113
- return messageModel.query(input, {
97
+ return messageModel.query(queryParams, {
98
+ groupAssistantMessages: useGroup ?? false,
114
99
  postProcessUrl: (path) => fileService.getFullFileUrl(path),
115
100
  });
116
101
  }),
@@ -1,6 +1,7 @@
1
1
  import { isDesktop, isServerMode } from '@lobechat/const';
2
2
  import {
3
3
  ContextEngine,
4
+ GroupMessageFlattenProcessor,
4
5
  HistorySummaryProvider,
5
6
  HistoryTruncateProcessor,
6
7
  InputTemplateProcessor,
@@ -28,7 +29,6 @@ interface ContextEngineeringContext {
28
29
  historyCount?: number;
29
30
  historySummary?: string;
30
31
  inputTemplate?: string;
31
- isWelcomeQuestion?: boolean;
32
32
  messages: UIChatMessage[];
33
33
  model: string;
34
34
  provider: string;
@@ -78,14 +78,15 @@ export const contextEngineering = async ({
78
78
  // Create message processing processors
79
79
 
80
80
  // 6. Input template processing
81
- new InputTemplateProcessor({
82
- inputTemplate,
83
- }),
81
+ new InputTemplateProcessor({ inputTemplate }),
84
82
 
85
83
  // 7. Placeholder variables processing
86
84
  new PlaceholderVariablesProcessor({ variableGenerators: VARIABLE_GENERATORS }),
87
85
 
88
- // 8. Message content processing
86
+ // 8. Group message flatten (convert role=group to standard assistant + tool messages)
87
+ new GroupMessageFlattenProcessor(),
88
+
89
+ // 8.5 Message content processing
89
90
  new MessageContentProcessor({
90
91
  fileContext: { enabled: isServerMode, includeFileUrl: !isDesktop },
91
92
  isCanUseVideo,
@@ -3,6 +3,8 @@ import { ChatTranslate, UIChatMessage } from '@lobechat/types';
3
3
 
4
4
  import { INBOX_SESSION_ID } from '@/const/session';
5
5
  import { lambdaClient } from '@/libs/trpc/client';
6
+ import { useUserStore } from '@/store/user';
7
+ import { labPreferSelectors } from '@/store/user/selectors';
6
8
 
7
9
  import { IMessageService } from './type';
8
10
 
@@ -22,33 +24,31 @@ export class ServerService implements IMessageService {
22
24
  };
23
25
 
24
26
  getMessages: IMessageService['getMessages'] = async (sessionId, topicId, groupId) => {
27
+ // Get user lab preference for message grouping
28
+ const useGroup = labPreferSelectors.enableAssistantMessageGroup(useUserStore.getState());
29
+
25
30
  const data = await lambdaClient.message.getMessages.query({
26
31
  groupId,
27
32
  sessionId: this.toDbSessionId(sessionId),
28
33
  topicId,
34
+ useGroup,
29
35
  });
30
36
 
31
37
  return data as unknown as UIChatMessage[];
32
38
  };
33
39
 
34
40
  getGroupMessages: IMessageService['getGroupMessages'] = async (groupId, topicId) => {
41
+ // Get user lab preference for message grouping
42
+ const useGroup = labPreferSelectors.enableAssistantMessageGroup(useUserStore.getState());
43
+
35
44
  const data = await lambdaClient.message.getMessages.query({
36
45
  groupId,
37
46
  topicId,
47
+ useGroup,
38
48
  });
39
49
  return data as unknown as UIChatMessage[];
40
50
  };
41
51
 
42
- getAllMessages: IMessageService['getAllMessages'] = async () => {
43
- return lambdaClient.message.getAllMessages.query();
44
- };
45
-
46
- getAllMessagesInSession: IMessageService['getAllMessagesInSession'] = async (sessionId) => {
47
- return lambdaClient.message.getAllMessagesInSession.query({
48
- sessionId: this.toDbSessionId(sessionId),
49
- });
50
- };
51
-
52
52
  countMessages: IMessageService['countMessages'] = async (params) => {
53
53
  return lambdaClient.message.count.query(params);
54
54
  };
@@ -21,8 +21,6 @@ export interface IMessageService {
21
21
 
22
22
  getMessages(sessionId: string, topicId?: string, groupId?: string): Promise<UIChatMessage[]>;
23
23
  getGroupMessages(groupId: string, topicId?: string): Promise<UIChatMessage[]>;
24
- getAllMessages(): Promise<UIChatMessage[]>;
25
- getAllMessagesInSession(sessionId: string): Promise<UIChatMessage[]>;
26
24
  countMessages(params?: {
27
25
  endDate?: string;
28
26
  range?: [string, string];
@@ -541,7 +541,7 @@ describe('generateAIChatV2 actions', () => {
541
541
  result.current.cancelSendMessageInServer();
542
542
  });
543
543
 
544
- expect(mockAbort).toHaveBeenCalledWith('User cancelled sendMessageInServer operation');
544
+ expect(mockAbort).toHaveBeenCalledWith('User cancelled sendMessage operation');
545
545
  expect(
546
546
  result.current.mainSendMessageOperations[
547
547
  messageMapKey(TEST_IDS.SESSION_ID, TEST_IDS.TOPIC_ID)
@@ -571,7 +571,7 @@ describe('generateAIChatV2 actions', () => {
571
571
  result.current.cancelSendMessageInServer(customTopicId);
572
572
  });
573
573
 
574
- expect(mockAbort).toHaveBeenCalledWith('User cancelled sendMessageInServer operation');
574
+ expect(mockAbort).toHaveBeenCalledWith('User cancelled sendMessage operation');
575
575
  });
576
576
 
577
577
  it('should handle gracefully when operation does not exist', () => {
@@ -740,4 +740,311 @@ describe('generateAIChatV2 actions', () => {
740
740
  });
741
741
  });
742
742
  });
743
+
744
+ describe('callToolFollowAssistantMessage', () => {
745
+ const TOOL_RESULT_MSG_ID = 'tool-result-msg-id';
746
+ const ASSISTANT_BLOCK_ID = 'assistant-block-id';
747
+ const GROUP_MESSAGE_ID = 'group-message-id';
748
+ const TOOL_CALL_ID = 'tool-call-id';
749
+
750
+ beforeEach(() => {
751
+ // Reset mocks
752
+ vi.spyOn(messageService, 'createNewMessage').mockResolvedValue({
753
+ id: 'new-assistant-block-id',
754
+ messages: [] as any,
755
+ });
756
+ });
757
+
758
+ it('should find group message from tool result_msg_id in tools array', async () => {
759
+ const { result } = renderHook(() => useChatStore());
760
+ const dispatchSpy = vi.fn();
761
+
762
+ // Create a group message structure with tool results
763
+ const groupMessage: UIChatMessage = {
764
+ id: GROUP_MESSAGE_ID,
765
+ role: 'group',
766
+ content: '',
767
+ sessionId: TEST_IDS.SESSION_ID,
768
+ topicId: TEST_IDS.TOPIC_ID,
769
+ children: [
770
+ {
771
+ id: ASSISTANT_BLOCK_ID,
772
+ content: 'Assistant response',
773
+ tools: [
774
+ {
775
+ id: TOOL_CALL_ID,
776
+ type: 'builtin',
777
+ apiName: 'testTool',
778
+ identifier: 'test-tool',
779
+ arguments: '{}',
780
+ result: {
781
+ id: TOOL_RESULT_MSG_ID,
782
+ content: 'Tool result',
783
+ },
784
+ result_msg_id: TOOL_RESULT_MSG_ID,
785
+ },
786
+ ],
787
+ },
788
+ ],
789
+ } as any;
790
+
791
+ act(() => {
792
+ useChatStore.setState({
793
+ activeId: TEST_IDS.SESSION_ID,
794
+ activeTopicId: TEST_IDS.TOPIC_ID,
795
+ messagesMap: {
796
+ [messageMapKey(TEST_IDS.SESSION_ID, TEST_IDS.TOPIC_ID)]: [groupMessage],
797
+ },
798
+ internal_execAgentRuntime: vi.fn(),
799
+ internal_dispatchMessage: dispatchSpy,
800
+ });
801
+ });
802
+
803
+ await act(async () => {
804
+ await result.current.callToolFollowAssistantMessage({
805
+ parentId: TOOL_RESULT_MSG_ID,
806
+ });
807
+ });
808
+
809
+ // Verify that addGroupBlock was called with the correct groupMessageId
810
+ expect(dispatchSpy).toHaveBeenCalledWith(
811
+ expect.objectContaining({
812
+ type: 'addGroupBlock',
813
+ groupMessageId: GROUP_MESSAGE_ID,
814
+ }),
815
+ );
816
+
817
+ // Verify that createNewMessage was called with message params
818
+ expect(messageService.createNewMessage).toHaveBeenCalledWith(
819
+ expect.objectContaining({
820
+ role: 'assistant',
821
+ parentId: TOOL_RESULT_MSG_ID,
822
+ }),
823
+ );
824
+ });
825
+
826
+ it('should handle case when tool result is not found in any group message', async () => {
827
+ const { result } = renderHook(() => useChatStore());
828
+ const dispatchSpy = vi.fn();
829
+
830
+ const groupMessage: UIChatMessage = {
831
+ id: GROUP_MESSAGE_ID,
832
+ role: 'group',
833
+ content: '',
834
+ sessionId: TEST_IDS.SESSION_ID,
835
+ children: [
836
+ {
837
+ id: ASSISTANT_BLOCK_ID,
838
+ content: 'Assistant response',
839
+ tools: [], // No tools
840
+ },
841
+ ],
842
+ } as any;
843
+
844
+ act(() => {
845
+ useChatStore.setState({
846
+ activeId: TEST_IDS.SESSION_ID,
847
+ activeTopicId: TEST_IDS.TOPIC_ID,
848
+ messagesMap: {
849
+ [messageMapKey(TEST_IDS.SESSION_ID, TEST_IDS.TOPIC_ID)]: [groupMessage],
850
+ },
851
+ internal_execAgentRuntime: vi.fn(),
852
+ internal_dispatchMessage: dispatchSpy,
853
+ });
854
+ });
855
+
856
+ await act(async () => {
857
+ await result.current.callToolFollowAssistantMessage({
858
+ parentId: 'non-existent-tool-result-id',
859
+ });
860
+ });
861
+
862
+ // Should create message as regular top-level message (createMessage, not addGroupBlock)
863
+ expect(dispatchSpy).toHaveBeenCalledWith(
864
+ expect.objectContaining({
865
+ type: 'createMessage',
866
+ }),
867
+ );
868
+
869
+ expect(messageService.createNewMessage).toHaveBeenCalledWith(
870
+ expect.objectContaining({
871
+ role: 'assistant',
872
+ parentId: 'non-existent-tool-result-id',
873
+ }),
874
+ );
875
+ });
876
+
877
+ it('should find group message from nested tool results in multiple children', async () => {
878
+ const { result } = renderHook(() => useChatStore());
879
+ const dispatchSpy = vi.fn();
880
+
881
+ const groupMessage: UIChatMessage = {
882
+ id: GROUP_MESSAGE_ID,
883
+ role: 'group',
884
+ content: '',
885
+ sessionId: TEST_IDS.SESSION_ID,
886
+ children: [
887
+ {
888
+ id: 'first-block',
889
+ content: 'First assistant response',
890
+ tools: [
891
+ {
892
+ id: 'tool-1',
893
+ type: 'builtin',
894
+ apiName: 'tool1',
895
+ identifier: 'tool-1',
896
+ arguments: '{}',
897
+ result_msg_id: 'other-result-id',
898
+ },
899
+ ],
900
+ },
901
+ {
902
+ id: 'second-block',
903
+ content: 'Second assistant response',
904
+ tools: [
905
+ {
906
+ id: 'tool-2',
907
+ type: 'builtin',
908
+ apiName: 'tool2',
909
+ identifier: 'tool-2',
910
+ arguments: '{}',
911
+ result_msg_id: TOOL_RESULT_MSG_ID, // Target tool result
912
+ },
913
+ ],
914
+ },
915
+ ],
916
+ } as any;
917
+
918
+ act(() => {
919
+ useChatStore.setState({
920
+ activeId: TEST_IDS.SESSION_ID,
921
+ messagesMap: {
922
+ [messageMapKey(TEST_IDS.SESSION_ID, TEST_IDS.TOPIC_ID)]: [groupMessage],
923
+ },
924
+ internal_execAgentRuntime: vi.fn(),
925
+ internal_dispatchMessage: dispatchSpy,
926
+ });
927
+ });
928
+
929
+ await act(async () => {
930
+ await result.current.callToolFollowAssistantMessage({
931
+ parentId: TOOL_RESULT_MSG_ID,
932
+ });
933
+ });
934
+
935
+ // Should find the correct group message even with multiple children
936
+ expect(dispatchSpy).toHaveBeenCalledWith(
937
+ expect.objectContaining({
938
+ type: 'addGroupBlock',
939
+ groupMessageId: GROUP_MESSAGE_ID,
940
+ }),
941
+ );
942
+ });
943
+
944
+ it('should call internal_execAgentRuntime after creating assistant message', async () => {
945
+ const { result } = renderHook(() => useChatStore());
946
+ const mockExecAgentRuntime = vi.fn();
947
+
948
+ const groupMessage: UIChatMessage = {
949
+ id: GROUP_MESSAGE_ID,
950
+ role: 'group',
951
+ content: '',
952
+ sessionId: TEST_IDS.SESSION_ID,
953
+ children: [
954
+ {
955
+ id: ASSISTANT_BLOCK_ID,
956
+ content: 'Response',
957
+ tools: [
958
+ {
959
+ id: TOOL_CALL_ID,
960
+ type: 'builtin',
961
+ apiName: 'test',
962
+ identifier: 'test',
963
+ arguments: '{}',
964
+ result_msg_id: TOOL_RESULT_MSG_ID,
965
+ },
966
+ ],
967
+ },
968
+ ],
969
+ } as any;
970
+
971
+ act(() => {
972
+ useChatStore.setState({
973
+ activeId: TEST_IDS.SESSION_ID,
974
+ messagesMap: {
975
+ [messageMapKey(TEST_IDS.SESSION_ID, TEST_IDS.TOPIC_ID)]: [groupMessage],
976
+ },
977
+ internal_execAgentRuntime: mockExecAgentRuntime,
978
+ });
979
+ });
980
+
981
+ await act(async () => {
982
+ await result.current.callToolFollowAssistantMessage({
983
+ parentId: TOOL_RESULT_MSG_ID,
984
+ traceId: 'test-trace-id',
985
+ threadId: 'test-thread-id',
986
+ });
987
+ });
988
+
989
+ expect(mockExecAgentRuntime).toHaveBeenCalledWith(
990
+ expect.objectContaining({
991
+ assistantMessageId: 'new-assistant-block-id',
992
+ traceId: 'test-trace-id',
993
+ threadId: 'test-thread-id',
994
+ }),
995
+ );
996
+ });
997
+
998
+ it('should handle missing result_msg_id field gracefully', async () => {
999
+ const { result } = renderHook(() => useChatStore());
1000
+ const dispatchSpy = vi.fn();
1001
+
1002
+ const groupMessage: UIChatMessage = {
1003
+ id: GROUP_MESSAGE_ID,
1004
+ role: 'group',
1005
+ content: '',
1006
+ sessionId: TEST_IDS.SESSION_ID,
1007
+ children: [
1008
+ {
1009
+ id: ASSISTANT_BLOCK_ID,
1010
+ content: 'Response',
1011
+ tools: [
1012
+ {
1013
+ id: TOOL_CALL_ID,
1014
+ type: 'builtin',
1015
+ apiName: 'test',
1016
+ identifier: 'test',
1017
+ arguments: '{}',
1018
+ // Missing result_msg_id
1019
+ },
1020
+ ],
1021
+ },
1022
+ ],
1023
+ } as any;
1024
+
1025
+ act(() => {
1026
+ useChatStore.setState({
1027
+ activeId: TEST_IDS.SESSION_ID,
1028
+ messagesMap: {
1029
+ [messageMapKey(TEST_IDS.SESSION_ID, TEST_IDS.TOPIC_ID)]: [groupMessage],
1030
+ },
1031
+ internal_execAgentRuntime: vi.fn(),
1032
+ internal_dispatchMessage: dispatchSpy,
1033
+ });
1034
+ });
1035
+
1036
+ await act(async () => {
1037
+ await result.current.callToolFollowAssistantMessage({
1038
+ parentId: TOOL_RESULT_MSG_ID,
1039
+ });
1040
+ });
1041
+
1042
+ // Should create as regular message since no groupMessageId found
1043
+ expect(dispatchSpy).toHaveBeenCalledWith(
1044
+ expect.objectContaining({
1045
+ type: 'createMessage',
1046
+ }),
1047
+ );
1048
+ });
1049
+ });
743
1050
  });