@copilotkit/react-core 1.54.1-next.6 → 1.55.0-next.7

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 (183) hide show
  1. package/CHANGELOG.md +121 -106
  2. package/dist/copilotkit-B3Mb1yVE.cjs +7975 -0
  3. package/dist/copilotkit-B3Mb1yVE.cjs.map +1 -0
  4. package/dist/copilotkit-DBzgOMby.d.cts +2182 -0
  5. package/dist/copilotkit-DBzgOMby.d.cts.map +1 -0
  6. package/dist/copilotkit-DNYSFuz5.mjs +7562 -0
  7. package/dist/copilotkit-DNYSFuz5.mjs.map +1 -0
  8. package/dist/copilotkit-Dy5w3qEV.d.mts +2182 -0
  9. package/dist/copilotkit-Dy5w3qEV.d.mts.map +1 -0
  10. package/dist/index.cjs +27 -28
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +3 -3
  13. package/dist/index.d.cts.map +1 -1
  14. package/dist/index.d.mts +3 -3
  15. package/dist/index.d.mts.map +1 -1
  16. package/dist/index.mjs +4 -5
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/index.umd.js +1941 -35
  19. package/dist/index.umd.js.map +1 -1
  20. package/dist/v2/index.cjs +77 -7
  21. package/dist/v2/index.css +1 -2
  22. package/dist/v2/index.d.cts +6 -4
  23. package/dist/v2/index.d.mts +6 -4
  24. package/dist/v2/index.mjs +7 -4
  25. package/dist/v2/index.umd.js +5725 -24
  26. package/dist/v2/index.umd.js.map +1 -1
  27. package/package.json +35 -7
  28. package/scripts/scope-preflight.mjs +101 -0
  29. package/src/components/CopilotListeners.tsx +2 -6
  30. package/src/components/copilot-provider/copilot-messages.tsx +1 -1
  31. package/src/components/copilot-provider/copilotkit-props.tsx +1 -1
  32. package/src/components/copilot-provider/copilotkit.tsx +4 -4
  33. package/src/context/copilot-messages-context.tsx +1 -1
  34. package/src/hooks/__tests__/use-coagent-config.test.ts +2 -2
  35. package/src/hooks/__tests__/use-coagent-state-render.e2e.test.tsx +2 -2
  36. package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +3 -7
  37. package/src/hooks/__tests__/use-frontend-tool-available.test.tsx +1 -1
  38. package/src/hooks/__tests__/use-frontend-tool-remount.e2e.test.tsx +4 -4
  39. package/src/hooks/use-agent-nodename.ts +1 -1
  40. package/src/hooks/use-coagent-state-render-bridge.tsx +1 -4
  41. package/src/hooks/use-coagent.ts +1 -1
  42. package/src/hooks/use-configure-chat-suggestions.tsx +2 -2
  43. package/src/hooks/use-copilot-chat-suggestions.tsx +2 -2
  44. package/src/hooks/use-copilot-chat_internal.ts +2 -2
  45. package/src/hooks/use-copilot-readable.ts +1 -1
  46. package/src/hooks/use-frontend-tool.ts +2 -2
  47. package/src/hooks/use-human-in-the-loop.ts +2 -2
  48. package/src/hooks/use-langgraph-interrupt.ts +2 -5
  49. package/src/hooks/use-lazy-tool-renderer.tsx +1 -1
  50. package/src/hooks/use-render-tool-call.ts +1 -1
  51. package/src/lib/copilot-task.ts +1 -1
  52. package/src/setupTests.ts +18 -14
  53. package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +176 -0
  54. package/src/v2/__tests__/globalSetup.ts +14 -0
  55. package/src/v2/__tests__/setup.ts +93 -0
  56. package/src/v2/__tests__/utils/test-helpers.tsx +470 -0
  57. package/src/v2/a2ui/A2UIMessageRenderer.tsx +206 -0
  58. package/src/v2/components/CopilotKitInspector.tsx +50 -0
  59. package/src/v2/components/MCPAppsActivityRenderer.tsx +785 -0
  60. package/src/v2/components/WildcardToolCallRender.tsx +86 -0
  61. package/src/v2/components/__tests__/license-warning-banner.test.tsx +46 -0
  62. package/src/v2/components/chat/CopilotChat.tsx +431 -0
  63. package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +375 -0
  64. package/src/v2/components/chat/CopilotChatAudioRecorder.tsx +350 -0
  65. package/src/v2/components/chat/CopilotChatInput.tsx +1302 -0
  66. package/src/v2/components/chat/CopilotChatMessageView.tsx +556 -0
  67. package/src/v2/components/chat/CopilotChatReasoningMessage.tsx +252 -0
  68. package/src/v2/components/chat/CopilotChatSuggestionPill.tsx +59 -0
  69. package/src/v2/components/chat/CopilotChatSuggestionView.tsx +133 -0
  70. package/src/v2/components/chat/CopilotChatToggleButton.tsx +171 -0
  71. package/src/v2/components/chat/CopilotChatToolCallsView.tsx +40 -0
  72. package/src/v2/components/chat/CopilotChatUserMessage.tsx +388 -0
  73. package/src/v2/components/chat/CopilotChatView.tsx +598 -0
  74. package/src/v2/components/chat/CopilotModalHeader.tsx +129 -0
  75. package/src/v2/components/chat/CopilotPopup.tsx +81 -0
  76. package/src/v2/components/chat/CopilotPopupView.tsx +317 -0
  77. package/src/v2/components/chat/CopilotSidebar.tsx +76 -0
  78. package/src/v2/components/chat/CopilotSidebarView.tsx +255 -0
  79. package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.tsx +1113 -0
  80. package/src/v2/components/chat/__tests__/CopilotChat.onError.test.tsx +73 -0
  81. package/src/v2/components/chat/__tests__/CopilotChat.slots.e2e.test.tsx +432 -0
  82. package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +150 -0
  83. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.slots.e2e.test.tsx +624 -0
  84. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.tsx +702 -0
  85. package/src/v2/components/chat/__tests__/CopilotChatCssClasses.test.tsx +107 -0
  86. package/src/v2/components/chat/__tests__/CopilotChatInput.slots.e2e.test.tsx +929 -0
  87. package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +986 -0
  88. package/src/v2/components/chat/__tests__/CopilotChatMessageView.slots.e2e.test.tsx +1004 -0
  89. package/src/v2/components/chat/__tests__/CopilotChatMessageView.test.tsx +169 -0
  90. package/src/v2/components/chat/__tests__/CopilotChatSuggestionView.slots.e2e.test.tsx +530 -0
  91. package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.tsx +782 -0
  92. package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +2413 -0
  93. package/src/v2/components/chat/__tests__/CopilotChatUserMessage.slots.e2e.test.tsx +621 -0
  94. package/src/v2/components/chat/__tests__/CopilotChatView.onClick.e2e.test.tsx +853 -0
  95. package/src/v2/components/chat/__tests__/CopilotChatView.slots.e2e.test.tsx +1050 -0
  96. package/src/v2/components/chat/__tests__/CopilotModalHeader.slots.e2e.test.tsx +484 -0
  97. package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.tsx +612 -0
  98. package/src/v2/components/chat/__tests__/CopilotSidebarView.slots.e2e.test.tsx +502 -0
  99. package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +1011 -0
  100. package/src/v2/components/chat/__tests__/setup.ts +1 -0
  101. package/src/v2/components/chat/index.ts +79 -0
  102. package/src/v2/components/index.ts +7 -0
  103. package/src/v2/components/license-warning-banner.tsx +198 -0
  104. package/src/v2/components/ui/button.tsx +123 -0
  105. package/src/v2/components/ui/dropdown-menu.tsx +258 -0
  106. package/src/v2/components/ui/tooltip.tsx +60 -0
  107. package/src/v2/hooks/__tests__/standard-schema-types.test.tsx +152 -0
  108. package/src/v2/hooks/__tests__/standard-schema.test.tsx +282 -0
  109. package/src/v2/hooks/__tests__/use-agent-context-timing.e2e.test.tsx +132 -0
  110. package/src/v2/hooks/__tests__/use-agent-context.test.tsx +401 -0
  111. package/src/v2/hooks/__tests__/use-agent-error-state.test.tsx +44 -0
  112. package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +205 -0
  113. package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +148 -0
  114. package/src/v2/hooks/__tests__/use-component.test.tsx +123 -0
  115. package/src/v2/hooks/__tests__/use-configure-suggestions.e2e.test.tsx +696 -0
  116. package/src/v2/hooks/__tests__/use-default-render-tool.test.tsx +153 -0
  117. package/src/v2/hooks/__tests__/use-frontend-tool-available.test.tsx +167 -0
  118. package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +2129 -0
  119. package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.tsx +1261 -0
  120. package/src/v2/hooks/__tests__/use-interrupt.test.tsx +397 -0
  121. package/src/v2/hooks/__tests__/use-katex-styles.test.tsx +56 -0
  122. package/src/v2/hooks/__tests__/use-keyboard-height.test.tsx +192 -0
  123. package/src/v2/hooks/__tests__/use-render-tool.test.tsx +259 -0
  124. package/src/v2/hooks/__tests__/use-suggestions.e2e.test.tsx +524 -0
  125. package/src/v2/hooks/__tests__/use-threads.test.tsx +433 -0
  126. package/src/v2/hooks/__tests__/zod-regression.test.tsx +311 -0
  127. package/src/v2/hooks/index.ts +18 -0
  128. package/src/v2/hooks/use-agent-context.tsx +45 -0
  129. package/src/v2/hooks/use-agent.tsx +155 -0
  130. package/src/v2/hooks/use-component.tsx +89 -0
  131. package/src/v2/hooks/use-configure-suggestions.tsx +187 -0
  132. package/src/v2/hooks/use-default-render-tool.tsx +254 -0
  133. package/src/v2/hooks/use-frontend-tool.tsx +43 -0
  134. package/src/v2/hooks/use-human-in-the-loop.tsx +81 -0
  135. package/src/v2/hooks/use-interrupt.tsx +305 -0
  136. package/src/v2/hooks/use-keyboard-height.tsx +67 -0
  137. package/src/v2/hooks/use-render-activity-message.tsx +73 -0
  138. package/src/v2/hooks/use-render-custom-messages.tsx +93 -0
  139. package/src/v2/hooks/use-render-tool-call.tsx +175 -0
  140. package/src/v2/hooks/use-render-tool.tsx +181 -0
  141. package/src/v2/hooks/use-suggestions.tsx +91 -0
  142. package/src/v2/hooks/use-threads.tsx +256 -0
  143. package/src/v2/hooks/useKatexStyles.ts +27 -0
  144. package/src/v2/index.css +1 -1
  145. package/src/v2/index.ts +18 -2
  146. package/src/v2/lib/__tests__/completePartialMarkdown.test.ts +495 -0
  147. package/src/v2/lib/__tests__/renderSlot.test.tsx +588 -0
  148. package/src/v2/lib/react-core.ts +156 -0
  149. package/src/v2/lib/slots.tsx +143 -0
  150. package/src/v2/lib/transcription-client.ts +184 -0
  151. package/src/v2/lib/utils.ts +8 -0
  152. package/src/v2/providers/CopilotChatConfigurationProvider.tsx +162 -0
  153. package/src/v2/providers/CopilotKitProvider.tsx +600 -0
  154. package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.tsx +546 -0
  155. package/src/v2/providers/__tests__/CopilotKitProvider.license.test.tsx +101 -0
  156. package/src/v2/providers/__tests__/CopilotKitProvider.onError.test.tsx +69 -0
  157. package/src/v2/providers/__tests__/CopilotKitProvider.renderCustomMessages.e2e.test.tsx +881 -0
  158. package/src/v2/providers/__tests__/CopilotKitProvider.stability.test.tsx +740 -0
  159. package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +642 -0
  160. package/src/v2/providers/__tests__/CopilotKitProvider.wildcard.test.tsx +294 -0
  161. package/src/v2/providers/index.ts +14 -0
  162. package/src/v2/styles/globals.css +230 -0
  163. package/src/v2/types/__tests__/defineToolCallRenderer.test.tsx +525 -0
  164. package/src/v2/types/defineToolCallRenderer.ts +65 -0
  165. package/src/v2/types/frontend-tool.ts +8 -0
  166. package/src/v2/types/human-in-the-loop.ts +33 -0
  167. package/src/v2/types/index.ts +7 -0
  168. package/src/v2/types/interrupt.ts +15 -0
  169. package/src/v2/types/react-activity-message-renderer.ts +27 -0
  170. package/src/v2/types/react-custom-message-renderer.ts +17 -0
  171. package/src/v2/types/react-tool-call-renderer.ts +32 -0
  172. package/tsdown.config.ts +34 -10
  173. package/vitest.config.mjs +4 -3
  174. package/LICENSE +0 -21
  175. package/dist/copilotkit-BRPQ2sqS.d.cts +0 -670
  176. package/dist/copilotkit-BRPQ2sqS.d.cts.map +0 -1
  177. package/dist/copilotkit-C94ayZbs.cjs +0 -2161
  178. package/dist/copilotkit-C94ayZbs.cjs.map +0 -1
  179. package/dist/copilotkit-CwZMFmSK.d.mts +0 -670
  180. package/dist/copilotkit-CwZMFmSK.d.mts.map +0 -1
  181. package/dist/copilotkit-Yh_Ld_FX.mjs +0 -2031
  182. package/dist/copilotkit-Yh_Ld_FX.mjs.map +0 -1
  183. package/dist/v2/index.css.map +0 -1
@@ -0,0 +1,556 @@
1
+ import React, { useEffect, useReducer, useState } from "react";
2
+ import { WithSlots, renderSlot, isReactComponentType } from "../../lib/slots";
3
+ import CopilotChatAssistantMessage from "./CopilotChatAssistantMessage";
4
+ import CopilotChatUserMessage from "./CopilotChatUserMessage";
5
+ import CopilotChatReasoningMessage from "./CopilotChatReasoningMessage";
6
+ import {
7
+ ActivityMessage,
8
+ AssistantMessage,
9
+ Message,
10
+ ReasoningMessage,
11
+ UserMessage,
12
+ } from "@ag-ui/core";
13
+ import { twMerge } from "tailwind-merge";
14
+ import { useRenderActivityMessage, useRenderCustomMessages } from "../../hooks";
15
+ import { useCopilotKit } from "../../providers/CopilotKitProvider";
16
+ import { useCopilotChatConfiguration } from "../../providers/CopilotChatConfigurationProvider";
17
+
18
+ /**
19
+ * Memoized wrapper for assistant messages to prevent re-renders when other messages change.
20
+ */
21
+ const MemoizedAssistantMessage = React.memo(
22
+ function MemoizedAssistantMessage({
23
+ message,
24
+ messages,
25
+ isRunning,
26
+ AssistantMessageComponent,
27
+ slotProps,
28
+ }: {
29
+ message: AssistantMessage;
30
+ messages: Message[];
31
+ isRunning: boolean;
32
+ AssistantMessageComponent: typeof CopilotChatAssistantMessage;
33
+ slotProps?: Partial<
34
+ React.ComponentProps<typeof CopilotChatAssistantMessage>
35
+ >;
36
+ }) {
37
+ return (
38
+ <AssistantMessageComponent
39
+ message={message}
40
+ messages={messages}
41
+ isRunning={isRunning}
42
+ {...slotProps}
43
+ />
44
+ );
45
+ },
46
+ (prevProps, nextProps) => {
47
+ // Only re-render if this specific message changed
48
+ if (prevProps.message.id !== nextProps.message.id) return false;
49
+ if (prevProps.message.content !== nextProps.message.content) return false;
50
+
51
+ // Compare tool calls if present
52
+ const prevToolCalls = prevProps.message.toolCalls;
53
+ const nextToolCalls = nextProps.message.toolCalls;
54
+ if (prevToolCalls?.length !== nextToolCalls?.length) return false;
55
+ if (prevToolCalls && nextToolCalls) {
56
+ for (let i = 0; i < prevToolCalls.length; i++) {
57
+ const prevTc = prevToolCalls[i];
58
+ const nextTc = nextToolCalls[i];
59
+ if (!prevTc || !nextTc) return false;
60
+ if (prevTc.id !== nextTc.id) return false;
61
+ if (prevTc.function.arguments !== nextTc.function.arguments)
62
+ return false;
63
+ }
64
+ }
65
+
66
+ // Check if tool results changed for this message's tool calls
67
+ // Tool results are separate messages with role="tool" that reference tool call IDs
68
+ if (prevToolCalls && prevToolCalls.length > 0) {
69
+ const toolCallIds = new Set(prevToolCalls.map((tc) => tc.id));
70
+
71
+ const prevToolResults = prevProps.messages.filter(
72
+ (m) => m.role === "tool" && toolCallIds.has((m as any).toolCallId),
73
+ );
74
+ const nextToolResults = nextProps.messages.filter(
75
+ (m) => m.role === "tool" && toolCallIds.has((m as any).toolCallId),
76
+ );
77
+
78
+ // If number of tool results changed, re-render
79
+ if (prevToolResults.length !== nextToolResults.length) return false;
80
+
81
+ // If any tool result content changed, re-render
82
+ for (let i = 0; i < prevToolResults.length; i++) {
83
+ if (
84
+ (prevToolResults[i] as any).content !==
85
+ (nextToolResults[i] as any).content
86
+ )
87
+ return false;
88
+ }
89
+ }
90
+
91
+ // Only care about isRunning if this message is CURRENTLY the latest
92
+ // (we don't need to re-render just because a message stopped being the latest)
93
+ const nextIsLatest =
94
+ nextProps.messages[nextProps.messages.length - 1]?.id ===
95
+ nextProps.message.id;
96
+ if (nextIsLatest && prevProps.isRunning !== nextProps.isRunning)
97
+ return false;
98
+
99
+ // Check if component reference changed
100
+ if (
101
+ prevProps.AssistantMessageComponent !==
102
+ nextProps.AssistantMessageComponent
103
+ )
104
+ return false;
105
+
106
+ // Check if slot props changed
107
+ if (prevProps.slotProps !== nextProps.slotProps) return false;
108
+
109
+ return true;
110
+ },
111
+ );
112
+
113
+ /**
114
+ * Memoized wrapper for user messages to prevent re-renders when other messages change.
115
+ */
116
+ const MemoizedUserMessage = React.memo(
117
+ function MemoizedUserMessage({
118
+ message,
119
+ UserMessageComponent,
120
+ slotProps,
121
+ }: {
122
+ message: UserMessage;
123
+ UserMessageComponent: typeof CopilotChatUserMessage;
124
+ slotProps?: Partial<React.ComponentProps<typeof CopilotChatUserMessage>>;
125
+ }) {
126
+ return <UserMessageComponent message={message} {...slotProps} />;
127
+ },
128
+ (prevProps, nextProps) => {
129
+ // Only re-render if this specific message changed
130
+ if (prevProps.message.id !== nextProps.message.id) return false;
131
+ if (prevProps.message.content !== nextProps.message.content) return false;
132
+ if (prevProps.UserMessageComponent !== nextProps.UserMessageComponent)
133
+ return false;
134
+ // Check if slot props changed
135
+ if (prevProps.slotProps !== nextProps.slotProps) return false;
136
+ return true;
137
+ },
138
+ );
139
+
140
+ /**
141
+ * Memoized wrapper for activity messages to prevent re-renders when other messages change.
142
+ */
143
+ const MemoizedActivityMessage = React.memo(
144
+ function MemoizedActivityMessage({
145
+ message,
146
+ renderActivityMessage,
147
+ }: {
148
+ message: ActivityMessage;
149
+ renderActivityMessage: (
150
+ message: ActivityMessage,
151
+ ) => React.ReactElement | null;
152
+ }) {
153
+ return renderActivityMessage(message);
154
+ },
155
+ (prevProps, nextProps) => {
156
+ // Message ID changed = different message, must re-render
157
+ if (prevProps.message.id !== nextProps.message.id) return false;
158
+
159
+ // Activity type changed = must re-render
160
+ if (prevProps.message.activityType !== nextProps.message.activityType)
161
+ return false;
162
+
163
+ // Compare content using JSON.stringify (native code, handles deep comparison)
164
+ if (
165
+ JSON.stringify(prevProps.message.content) !==
166
+ JSON.stringify(nextProps.message.content)
167
+ )
168
+ return false;
169
+
170
+ return true;
171
+ },
172
+ );
173
+
174
+ /**
175
+ * Memoized wrapper for reasoning messages to prevent re-renders when other messages change.
176
+ */
177
+ const MemoizedReasoningMessage = React.memo(
178
+ function MemoizedReasoningMessage({
179
+ message,
180
+ messages,
181
+ isRunning,
182
+ ReasoningMessageComponent,
183
+ slotProps,
184
+ }: {
185
+ message: ReasoningMessage;
186
+ messages: Message[];
187
+ isRunning: boolean;
188
+ ReasoningMessageComponent: typeof CopilotChatReasoningMessage;
189
+ slotProps?: Partial<
190
+ React.ComponentProps<typeof CopilotChatReasoningMessage>
191
+ >;
192
+ }) {
193
+ return (
194
+ <ReasoningMessageComponent
195
+ message={message}
196
+ messages={messages}
197
+ isRunning={isRunning}
198
+ {...slotProps}
199
+ />
200
+ );
201
+ },
202
+ (prevProps, nextProps) => {
203
+ // Only re-render if this specific message changed
204
+ if (prevProps.message.id !== nextProps.message.id) return false;
205
+ if (prevProps.message.content !== nextProps.message.content) return false;
206
+
207
+ // Re-render when "latest" status changes (e.g. reasoning message is no longer the last message
208
+ // because a text message was added after it — this transitions isStreaming from true to false)
209
+ const prevIsLatest =
210
+ prevProps.messages[prevProps.messages.length - 1]?.id ===
211
+ prevProps.message.id;
212
+ const nextIsLatest =
213
+ nextProps.messages[nextProps.messages.length - 1]?.id ===
214
+ nextProps.message.id;
215
+ if (prevIsLatest !== nextIsLatest) return false;
216
+
217
+ // Only care about isRunning if this message is CURRENTLY the latest
218
+ if (nextIsLatest && prevProps.isRunning !== nextProps.isRunning)
219
+ return false;
220
+
221
+ // Check if component reference changed
222
+ if (
223
+ prevProps.ReasoningMessageComponent !==
224
+ nextProps.ReasoningMessageComponent
225
+ )
226
+ return false;
227
+
228
+ // Check if slot props changed
229
+ if (prevProps.slotProps !== nextProps.slotProps) return false;
230
+
231
+ return true;
232
+ },
233
+ );
234
+
235
+ /**
236
+ * Memoized wrapper for custom messages to prevent re-renders when other messages change.
237
+ */
238
+ const MemoizedCustomMessage = React.memo(
239
+ function MemoizedCustomMessage({
240
+ message,
241
+ position,
242
+ renderCustomMessage,
243
+ }: {
244
+ message: Message;
245
+ position: "before" | "after";
246
+ renderCustomMessage: (params: {
247
+ message: Message;
248
+ position: "before" | "after";
249
+ }) => React.ReactElement | null;
250
+ stateSnapshot?: unknown;
251
+ }) {
252
+ return renderCustomMessage({ message, position });
253
+ },
254
+ (prevProps, nextProps) => {
255
+ // Only re-render if the message or position changed
256
+ if (prevProps.message.id !== nextProps.message.id) return false;
257
+ if (prevProps.position !== nextProps.position) return false;
258
+ // Compare message content - for assistant messages this is a string, for others may differ
259
+ if (prevProps.message.content !== nextProps.message.content) return false;
260
+ if (prevProps.message.role !== nextProps.message.role) return false;
261
+ // Compare state snapshot - custom renderers may depend on state
262
+ if (
263
+ JSON.stringify(prevProps.stateSnapshot) !==
264
+ JSON.stringify(nextProps.stateSnapshot)
265
+ )
266
+ return false;
267
+ // Note: We don't compare renderCustomMessage function reference because it changes
268
+ // frequently. The message and state comparison is sufficient to determine if a re-render is needed.
269
+ return true;
270
+ },
271
+ );
272
+
273
+ export type CopilotChatMessageViewProps = Omit<
274
+ WithSlots<
275
+ {
276
+ assistantMessage: typeof CopilotChatAssistantMessage;
277
+ userMessage: typeof CopilotChatUserMessage;
278
+ reasoningMessage: typeof CopilotChatReasoningMessage;
279
+ cursor: typeof CopilotChatMessageView.Cursor;
280
+ },
281
+ {
282
+ isRunning?: boolean;
283
+ messages?: Message[];
284
+ } & React.HTMLAttributes<HTMLDivElement>
285
+ >,
286
+ "children"
287
+ > & {
288
+ children?: (props: {
289
+ isRunning: boolean;
290
+ messages: Message[];
291
+ messageElements: React.ReactElement[];
292
+ interruptElement: React.ReactElement | null;
293
+ }) => React.ReactElement;
294
+ };
295
+
296
+ export function CopilotChatMessageView({
297
+ messages = [],
298
+ assistantMessage,
299
+ userMessage,
300
+ reasoningMessage,
301
+ cursor,
302
+ isRunning = false,
303
+ children,
304
+ className,
305
+ ...props
306
+ }: CopilotChatMessageViewProps) {
307
+ const renderCustomMessage = useRenderCustomMessages();
308
+ const { renderActivityMessage } = useRenderActivityMessage();
309
+ const { copilotkit } = useCopilotKit();
310
+ const config = useCopilotChatConfiguration();
311
+ const [, forceUpdate] = useReducer((x) => x + 1, 0);
312
+
313
+ // Subscribe to state changes so custom message renderers re-render when state updates.
314
+ useEffect(() => {
315
+ if (!config?.agentId) return;
316
+ const agent = copilotkit.getAgent(config.agentId);
317
+ if (!agent) return;
318
+
319
+ const subscription = agent.subscribe({
320
+ onStateChanged: forceUpdate,
321
+ });
322
+ return () => subscription.unsubscribe();
323
+ }, [config?.agentId, copilotkit, forceUpdate]);
324
+
325
+ // Subscribe to interrupt element changes for in-chat rendering.
326
+ const [interruptElement, setInterruptElement] =
327
+ useState<React.ReactElement | null>(null);
328
+ useEffect(() => {
329
+ setInterruptElement(copilotkit.interruptElement);
330
+ const subscription = copilotkit.subscribe({
331
+ onInterruptElementChanged: ({ interruptElement }) => {
332
+ setInterruptElement(interruptElement);
333
+ },
334
+ });
335
+ return () => subscription.unsubscribe();
336
+ }, [copilotkit]);
337
+
338
+ // Helper to get state snapshot for a message (used for memoization)
339
+ const getStateSnapshotForMessage = (messageId: string): unknown => {
340
+ if (!config) return undefined;
341
+ const resolvedRunId =
342
+ copilotkit.getRunIdForMessage(
343
+ config.agentId,
344
+ config.threadId,
345
+ messageId,
346
+ ) ??
347
+ copilotkit
348
+ .getRunIdsForThread(config.agentId, config.threadId)
349
+ .slice(-1)[0];
350
+ if (!resolvedRunId) return undefined;
351
+ return copilotkit.getStateByRun(
352
+ config.agentId,
353
+ config.threadId,
354
+ resolvedRunId,
355
+ );
356
+ };
357
+
358
+ // Deduplicate messages by id, keeping the last occurrence of each.
359
+ // During streaming, AbstractAgent.addMessage() can push duplicate messages
360
+ // (same id) which causes React "duplicate key" warnings and rendering glitches.
361
+ const deduplicatedMessages = [
362
+ ...new Map(messages.map((m) => [m.id, m])).values(),
363
+ ];
364
+
365
+ if (
366
+ process.env.NODE_ENV === "development" &&
367
+ deduplicatedMessages.length < messages.length
368
+ ) {
369
+ console.warn(
370
+ `CopilotChatMessageView: Deduplicated ${messages.length - deduplicatedMessages.length} message(s) with duplicate IDs.`,
371
+ );
372
+ }
373
+
374
+ const messageElements: React.ReactElement[] = deduplicatedMessages
375
+ .flatMap((message) => {
376
+ const elements: (React.ReactElement | null | undefined)[] = [];
377
+ const stateSnapshot = getStateSnapshotForMessage(message.id);
378
+
379
+ // Render custom message before (using memoized wrapper)
380
+ if (renderCustomMessage) {
381
+ elements.push(
382
+ <MemoizedCustomMessage
383
+ key={`${message.id}-custom-before`}
384
+ message={message}
385
+ position="before"
386
+ renderCustomMessage={renderCustomMessage}
387
+ stateSnapshot={stateSnapshot}
388
+ />,
389
+ );
390
+ }
391
+
392
+ // Render the main message using memoized wrappers to prevent unnecessary re-renders
393
+ if (message.role === "assistant") {
394
+ // Determine the component and props from slot value
395
+ let AssistantComponent = CopilotChatAssistantMessage;
396
+ let assistantSlotProps:
397
+ | Partial<React.ComponentProps<typeof CopilotChatAssistantMessage>>
398
+ | undefined;
399
+
400
+ if (isReactComponentType(assistantMessage)) {
401
+ // Custom component (function, forwardRef, memo, etc.)
402
+ AssistantComponent =
403
+ assistantMessage as typeof CopilotChatAssistantMessage;
404
+ } else if (typeof assistantMessage === "string") {
405
+ // className string
406
+ assistantSlotProps = { className: assistantMessage };
407
+ } else if (assistantMessage && typeof assistantMessage === "object") {
408
+ // Props object
409
+ assistantSlotProps = assistantMessage as Partial<
410
+ React.ComponentProps<typeof CopilotChatAssistantMessage>
411
+ >;
412
+ }
413
+
414
+ elements.push(
415
+ <MemoizedAssistantMessage
416
+ key={message.id}
417
+ message={message as AssistantMessage}
418
+ messages={messages}
419
+ isRunning={isRunning}
420
+ AssistantMessageComponent={AssistantComponent}
421
+ slotProps={assistantSlotProps}
422
+ />,
423
+ );
424
+ } else if (message.role === "user") {
425
+ // Determine the component and props from slot value
426
+ let UserComponent = CopilotChatUserMessage;
427
+ let userSlotProps:
428
+ | Partial<React.ComponentProps<typeof CopilotChatUserMessage>>
429
+ | undefined;
430
+
431
+ if (isReactComponentType(userMessage)) {
432
+ // Custom component (function, forwardRef, memo, etc.)
433
+ UserComponent = userMessage as typeof CopilotChatUserMessage;
434
+ } else if (typeof userMessage === "string") {
435
+ // className string
436
+ userSlotProps = { className: userMessage };
437
+ } else if (userMessage && typeof userMessage === "object") {
438
+ // Props object
439
+ userSlotProps = userMessage as Partial<
440
+ React.ComponentProps<typeof CopilotChatUserMessage>
441
+ >;
442
+ }
443
+
444
+ elements.push(
445
+ <MemoizedUserMessage
446
+ key={message.id}
447
+ message={message as UserMessage}
448
+ UserMessageComponent={UserComponent}
449
+ slotProps={userSlotProps}
450
+ />,
451
+ );
452
+ } else if (message.role === "activity") {
453
+ // Use memoized wrapper to prevent re-renders when other messages change
454
+ const activityMsg = message as ActivityMessage;
455
+ elements.push(
456
+ <MemoizedActivityMessage
457
+ key={message.id}
458
+ message={activityMsg}
459
+ renderActivityMessage={renderActivityMessage}
460
+ />,
461
+ );
462
+ } else if (message.role === "reasoning") {
463
+ // Determine the component and props from slot value
464
+ let ReasoningComponent = CopilotChatReasoningMessage;
465
+ let reasoningSlotProps:
466
+ | Partial<React.ComponentProps<typeof CopilotChatReasoningMessage>>
467
+ | undefined;
468
+
469
+ if (isReactComponentType(reasoningMessage)) {
470
+ ReasoningComponent =
471
+ reasoningMessage as typeof CopilotChatReasoningMessage;
472
+ } else if (typeof reasoningMessage === "string") {
473
+ reasoningSlotProps = { className: reasoningMessage };
474
+ } else if (reasoningMessage && typeof reasoningMessage === "object") {
475
+ reasoningSlotProps = reasoningMessage as Partial<
476
+ React.ComponentProps<typeof CopilotChatReasoningMessage>
477
+ >;
478
+ }
479
+
480
+ elements.push(
481
+ <MemoizedReasoningMessage
482
+ key={message.id}
483
+ message={message as ReasoningMessage}
484
+ messages={messages}
485
+ isRunning={isRunning}
486
+ ReasoningMessageComponent={ReasoningComponent}
487
+ slotProps={reasoningSlotProps}
488
+ />,
489
+ );
490
+ }
491
+
492
+ // Render custom message after (using memoized wrapper)
493
+ if (renderCustomMessage) {
494
+ elements.push(
495
+ <MemoizedCustomMessage
496
+ key={`${message.id}-custom-after`}
497
+ message={message}
498
+ position="after"
499
+ renderCustomMessage={renderCustomMessage}
500
+ stateSnapshot={stateSnapshot}
501
+ />,
502
+ );
503
+ }
504
+
505
+ return elements;
506
+ })
507
+ .filter(Boolean) as React.ReactElement[];
508
+
509
+ if (children) {
510
+ return (
511
+ <div data-copilotkit style={{ display: "contents" }}>
512
+ {children({ messageElements, messages, isRunning, interruptElement })}
513
+ </div>
514
+ );
515
+ }
516
+
517
+ // Hide the chat-level loading cursor when the last message is a reasoning
518
+ // message — the reasoning card already shows its own loading indicator.
519
+ const lastMessage = messages[messages.length - 1];
520
+ const showCursor = isRunning && lastMessage?.role !== "reasoning";
521
+
522
+ return (
523
+ <div
524
+ data-copilotkit
525
+ data-testid="copilot-message-list"
526
+ className={twMerge("copilotKitMessages cpk:flex cpk:flex-col", className)}
527
+ {...props}
528
+ >
529
+ {messageElements}
530
+ {interruptElement}
531
+ {showCursor && (
532
+ <div className="cpk:mt-2">
533
+ {renderSlot(cursor, CopilotChatMessageView.Cursor, {})}
534
+ </div>
535
+ )}
536
+ </div>
537
+ );
538
+ }
539
+
540
+ CopilotChatMessageView.Cursor = function Cursor({
541
+ className,
542
+ ...props
543
+ }: React.HTMLAttributes<HTMLDivElement>) {
544
+ return (
545
+ <div
546
+ data-testid="copilot-loading-cursor"
547
+ className={twMerge(
548
+ "cpk:w-[11px] cpk:h-[11px] cpk:rounded-full cpk:bg-foreground cpk:animate-pulse-cursor cpk:ml-1",
549
+ className,
550
+ )}
551
+ {...props}
552
+ />
553
+ );
554
+ };
555
+
556
+ export default CopilotChatMessageView;