@copilotkit/react-core 1.55.0-next.8 → 1.55.0
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 +48 -5
- package/dist/{copilotkit-DNYSFuz5.mjs → copilotkit-BY5S1-0P.mjs} +2772 -858
- package/dist/copilotkit-BY5S1-0P.mjs.map +1 -0
- package/dist/{copilotkit-Dy5w3qEV.d.mts → copilotkit-BuhSUZHb.d.mts} +230 -17
- package/dist/copilotkit-BuhSUZHb.d.mts.map +1 -0
- package/dist/{copilotkit-B3Mb1yVE.cjs → copilotkit-Bz5-ImDl.cjs} +2776 -832
- package/dist/copilotkit-Bz5-ImDl.cjs.map +1 -0
- package/dist/{copilotkit-DBzgOMby.d.cts → copilotkit-dwDWYpya.d.cts} +230 -17
- package/dist/copilotkit-dwDWYpya.d.cts.map +1 -0
- package/dist/index.cjs +9 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +9 -4
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +1624 -396
- package/dist/index.umd.js.map +1 -1
- package/dist/v2/index.cjs +13 -1
- package/dist/v2/index.css +1 -1
- package/dist/v2/index.d.cts +3 -3
- package/dist/v2/index.d.mts +3 -3
- package/dist/v2/index.mjs +3 -2
- package/dist/v2/index.umd.js +2746 -790
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +62 -54
- package/scripts/scope-preflight.mjs +1 -2
- package/src/components/CopilotListeners.tsx +41 -8
- package/src/components/copilot-provider/__tests__/copilot-messages-key.test.tsx +92 -0
- package/src/components/copilot-provider/copilotkit-props.tsx +4 -2
- package/src/components/copilot-provider/copilotkit.tsx +3 -3
- package/src/components/toast/toast-provider.tsx +269 -194
- package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +27 -16
- package/src/hooks/use-copilot-chat_internal.ts +15 -4
- package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +86 -22
- package/src/v2/__tests__/utils/test-helpers.tsx +107 -7
- package/src/v2/a2ui/A2UICatalogContext.tsx +79 -0
- package/src/v2/a2ui/A2UIMessageRenderer.tsx +125 -37
- package/src/v2/a2ui/A2UIToolCallRenderer.tsx +290 -0
- package/src/v2/components/CopilotKitInspector.tsx +2 -0
- package/src/v2/components/OpenGenerativeUIRenderer.tsx +598 -0
- package/src/v2/components/__tests__/OpenGenerativeUIRenderer.test.tsx +665 -0
- package/src/v2/components/chat/CopilotChat.tsx +197 -52
- package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +17 -2
- package/src/v2/components/chat/CopilotChatAttachmentQueue.tsx +481 -0
- package/src/v2/components/chat/CopilotChatAttachmentRenderer.tsx +139 -0
- package/src/v2/components/chat/CopilotChatInput.tsx +146 -77
- package/src/v2/components/chat/CopilotChatMessageView.tsx +260 -151
- package/src/v2/components/chat/CopilotChatSuggestionView.tsx +1 -0
- package/src/v2/components/chat/CopilotChatUserMessage.tsx +54 -0
- package/src/v2/components/chat/CopilotChatView.tsx +179 -66
- package/src/v2/components/chat/__tests__/CopilotChat.attachments.test.tsx +168 -0
- package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +63 -2
- package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +544 -1
- package/src/v2/components/chat/__tests__/CopilotChatPerf.e2e.test.tsx +268 -0
- package/src/v2/components/chat/__tests__/CopilotChatPropsRerender.e2e.test.tsx +249 -0
- package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.tsx +5 -2
- package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +5 -2
- package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +60 -3
- package/src/v2/components/chat/__tests__/copilot-chat-throttle.test.tsx +138 -0
- package/src/v2/components/chat/index.ts +9 -0
- package/src/v2/components/chat/scroll-element-context.ts +13 -0
- package/src/v2/hooks/__tests__/use-agent-context-timing.e2e.test.tsx +8 -0
- package/src/v2/hooks/__tests__/use-agent-thread-isolation.test.tsx +327 -0
- package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +1003 -0
- package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +13 -2
- package/src/v2/hooks/__tests__/use-attachments.test.tsx +169 -0
- package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +23 -4
- package/src/v2/hooks/__tests__/use-threads.test.tsx +54 -0
- package/src/v2/hooks/index.ts +5 -0
- package/src/v2/hooks/use-agent.tsx +220 -15
- package/src/v2/hooks/use-attachments.tsx +269 -0
- package/src/v2/hooks/use-frontend-tool.tsx +5 -2
- package/src/v2/hooks/use-render-activity-message.tsx +9 -2
- package/src/v2/hooks/use-render-custom-messages.tsx +6 -1
- package/src/v2/hooks/use-threads.tsx +35 -15
- package/src/v2/index.ts +5 -1
- package/src/v2/lib/__tests__/processPartialHtml.test.ts +112 -0
- package/src/v2/lib/__tests__/slots.test.ts +56 -0
- package/src/v2/lib/processPartialHtml.ts +45 -0
- package/src/v2/lib/slots.tsx +42 -1
- package/src/v2/providers/CopilotChatConfigurationProvider.tsx +9 -3
- package/src/v2/providers/CopilotKitProvider.tsx +268 -32
- package/src/v2/providers/SandboxFunctionsContext.ts +10 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.sandboxFunctions.test.tsx +198 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +71 -0
- package/src/v2/providers/index.ts +7 -0
- package/src/v2/styles/globals.css +2 -1
- package/src/v2/types/index.ts +1 -0
- package/src/v2/types/sandbox-function.ts +11 -0
- package/dist/copilotkit-B3Mb1yVE.cjs.map +0 -1
- package/dist/copilotkit-DBzgOMby.d.cts.map +0 -1
- package/dist/copilotkit-DNYSFuz5.mjs.map +0 -1
- package/dist/copilotkit-Dy5w3qEV.d.mts.map +0 -1
- package/src/v2/components/__tests__/license-warning-banner.test.tsx +0 -46
|
@@ -97,6 +97,269 @@ export function useToast() {
|
|
|
97
97
|
return context;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
function formatBannerMessage(message: string): string {
|
|
101
|
+
// Try to extract the useful message from JSON first
|
|
102
|
+
const jsonMatch = message.match(/'message':\s*'([^']+)'/);
|
|
103
|
+
if (jsonMatch) {
|
|
104
|
+
return jsonMatch[1];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Strip technical garbage but keep the meaningful message
|
|
108
|
+
let cleaned = message.split(" - ")[0];
|
|
109
|
+
cleaned = cleaned.split(": Error code")[0];
|
|
110
|
+
cleaned = cleaned.replace(/:\s*\d{3}$/, "");
|
|
111
|
+
cleaned = cleaned.replace(/See more:.*$/g, "");
|
|
112
|
+
cleaned = cleaned.trim();
|
|
113
|
+
|
|
114
|
+
return cleaned || "An error occurred.";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function extractUrl(message: string): { url: string; text: string } | null {
|
|
118
|
+
const markdownMatch = /\[([^\]]+)\]\(([^)]+)\)/.exec(message);
|
|
119
|
+
if (markdownMatch) {
|
|
120
|
+
return { url: markdownMatch[2], text: "See More" };
|
|
121
|
+
}
|
|
122
|
+
const plainMatch = /(https?:\/\/[^\s)]+)/.exec(message);
|
|
123
|
+
if (plainMatch) {
|
|
124
|
+
return {
|
|
125
|
+
url: plainMatch[0].replace(/[.,;:'"]*$/, ""),
|
|
126
|
+
text: "See More",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function BannerErrorDisplay({
|
|
133
|
+
bannerError,
|
|
134
|
+
onDismiss,
|
|
135
|
+
}: {
|
|
136
|
+
bannerError: CopilotKitError;
|
|
137
|
+
onDismiss: () => void;
|
|
138
|
+
}) {
|
|
139
|
+
const [detailsExpanded, setDetailsExpanded] = useState(false);
|
|
140
|
+
const severity = getErrorSeverity(bannerError);
|
|
141
|
+
const colors = getErrorColors(severity);
|
|
142
|
+
|
|
143
|
+
// Extract optional error details attached by CopilotListeners
|
|
144
|
+
const details = (bannerError as any).details as
|
|
145
|
+
| {
|
|
146
|
+
code?: string;
|
|
147
|
+
context?: Record<string, any>;
|
|
148
|
+
stack?: string;
|
|
149
|
+
originalMessage?: string;
|
|
150
|
+
}
|
|
151
|
+
| undefined;
|
|
152
|
+
|
|
153
|
+
const link = extractUrl(bannerError.message);
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div
|
|
157
|
+
style={{
|
|
158
|
+
position: "fixed",
|
|
159
|
+
bottom: "20px",
|
|
160
|
+
left: "50%",
|
|
161
|
+
transform: "translateX(-50%)",
|
|
162
|
+
zIndex: 9999,
|
|
163
|
+
backgroundColor: colors.background,
|
|
164
|
+
border: `1px solid ${colors.border}`,
|
|
165
|
+
borderLeft: `4px solid ${colors.border}`,
|
|
166
|
+
borderRadius: "8px",
|
|
167
|
+
padding: "12px 16px",
|
|
168
|
+
fontSize: "13px",
|
|
169
|
+
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
|
|
170
|
+
backdropFilter: "blur(8px)",
|
|
171
|
+
maxWidth: "min(90vw, 700px)",
|
|
172
|
+
width: "100%",
|
|
173
|
+
boxSizing: "border-box",
|
|
174
|
+
overflow: "hidden",
|
|
175
|
+
}}
|
|
176
|
+
>
|
|
177
|
+
<div
|
|
178
|
+
style={{
|
|
179
|
+
display: "flex",
|
|
180
|
+
justifyContent: "space-between",
|
|
181
|
+
alignItems: "center",
|
|
182
|
+
gap: "10px",
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
<div
|
|
186
|
+
style={{
|
|
187
|
+
display: "flex",
|
|
188
|
+
alignItems: "center",
|
|
189
|
+
gap: "8px",
|
|
190
|
+
flex: 1,
|
|
191
|
+
minWidth: 0,
|
|
192
|
+
}}
|
|
193
|
+
>
|
|
194
|
+
<div
|
|
195
|
+
style={{
|
|
196
|
+
width: "12px",
|
|
197
|
+
height: "12px",
|
|
198
|
+
borderRadius: "50%",
|
|
199
|
+
backgroundColor: colors.border,
|
|
200
|
+
flexShrink: 0,
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
203
|
+
<div
|
|
204
|
+
style={{
|
|
205
|
+
display: "flex",
|
|
206
|
+
alignItems: "center",
|
|
207
|
+
gap: "10px",
|
|
208
|
+
flex: 1,
|
|
209
|
+
minWidth: 0,
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
<div
|
|
213
|
+
style={{
|
|
214
|
+
color: colors.text,
|
|
215
|
+
lineHeight: "1.4",
|
|
216
|
+
fontWeight: "400",
|
|
217
|
+
fontSize: "13px",
|
|
218
|
+
flex: 1,
|
|
219
|
+
wordBreak: "break-all",
|
|
220
|
+
overflowWrap: "break-word",
|
|
221
|
+
maxWidth: "550px",
|
|
222
|
+
overflow: "hidden",
|
|
223
|
+
display: "-webkit-box",
|
|
224
|
+
WebkitLineClamp: 10,
|
|
225
|
+
WebkitBoxOrient: "vertical",
|
|
226
|
+
}}
|
|
227
|
+
>
|
|
228
|
+
{formatBannerMessage(bannerError.message)}
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
{link && (
|
|
232
|
+
<button
|
|
233
|
+
onClick={() =>
|
|
234
|
+
window.open(link.url, "_blank", "noopener,noreferrer")
|
|
235
|
+
}
|
|
236
|
+
style={{
|
|
237
|
+
background: colors.border,
|
|
238
|
+
color: "white",
|
|
239
|
+
border: "none",
|
|
240
|
+
borderRadius: "5px",
|
|
241
|
+
padding: "4px 10px",
|
|
242
|
+
fontSize: "11px",
|
|
243
|
+
fontWeight: "500",
|
|
244
|
+
cursor: "pointer",
|
|
245
|
+
transition: "all 0.2s ease",
|
|
246
|
+
flexShrink: 0,
|
|
247
|
+
}}
|
|
248
|
+
onMouseEnter={(e) => {
|
|
249
|
+
e.currentTarget.style.opacity = "0.9";
|
|
250
|
+
e.currentTarget.style.transform = "translateY(-1px)";
|
|
251
|
+
}}
|
|
252
|
+
onMouseLeave={(e) => {
|
|
253
|
+
e.currentTarget.style.opacity = "1";
|
|
254
|
+
e.currentTarget.style.transform = "translateY(0)";
|
|
255
|
+
}}
|
|
256
|
+
>
|
|
257
|
+
{link.text}
|
|
258
|
+
</button>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
{details && (
|
|
262
|
+
<button
|
|
263
|
+
onClick={() => setDetailsExpanded(!detailsExpanded)}
|
|
264
|
+
style={{
|
|
265
|
+
background: "transparent",
|
|
266
|
+
border: `1px solid ${colors.border}`,
|
|
267
|
+
borderRadius: "5px",
|
|
268
|
+
padding: "4px 10px",
|
|
269
|
+
fontSize: "11px",
|
|
270
|
+
fontWeight: "500",
|
|
271
|
+
cursor: "pointer",
|
|
272
|
+
color: colors.text,
|
|
273
|
+
flexShrink: 0,
|
|
274
|
+
transition: "all 0.2s ease",
|
|
275
|
+
}}
|
|
276
|
+
onMouseEnter={(e) => {
|
|
277
|
+
e.currentTarget.style.background = "rgba(0, 0, 0, 0.05)";
|
|
278
|
+
}}
|
|
279
|
+
onMouseLeave={(e) => {
|
|
280
|
+
e.currentTarget.style.background = "transparent";
|
|
281
|
+
}}
|
|
282
|
+
>
|
|
283
|
+
{detailsExpanded ? "Hide Details" : "Show Details"}
|
|
284
|
+
</button>
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
<button
|
|
289
|
+
onClick={onDismiss}
|
|
290
|
+
style={{
|
|
291
|
+
background: "transparent",
|
|
292
|
+
border: "none",
|
|
293
|
+
color: colors.text,
|
|
294
|
+
cursor: "pointer",
|
|
295
|
+
padding: "2px",
|
|
296
|
+
borderRadius: "3px",
|
|
297
|
+
fontSize: "14px",
|
|
298
|
+
lineHeight: "1",
|
|
299
|
+
opacity: 0.6,
|
|
300
|
+
transition: "all 0.2s ease",
|
|
301
|
+
flexShrink: 0,
|
|
302
|
+
}}
|
|
303
|
+
title="Dismiss"
|
|
304
|
+
onMouseEnter={(e) => {
|
|
305
|
+
e.currentTarget.style.opacity = "1";
|
|
306
|
+
e.currentTarget.style.background = "rgba(0, 0, 0, 0.05)";
|
|
307
|
+
}}
|
|
308
|
+
onMouseLeave={(e) => {
|
|
309
|
+
e.currentTarget.style.opacity = "0.6";
|
|
310
|
+
e.currentTarget.style.background = "transparent";
|
|
311
|
+
}}
|
|
312
|
+
>
|
|
313
|
+
x
|
|
314
|
+
</button>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
{detailsExpanded && details && (
|
|
318
|
+
<div
|
|
319
|
+
style={{
|
|
320
|
+
marginTop: "10px",
|
|
321
|
+
padding: "10px",
|
|
322
|
+
background: "rgba(0, 0, 0, 0.04)",
|
|
323
|
+
borderRadius: "6px",
|
|
324
|
+
fontSize: "11px",
|
|
325
|
+
fontFamily: "monospace",
|
|
326
|
+
color: colors.text,
|
|
327
|
+
lineHeight: "1.5",
|
|
328
|
+
maxHeight: "200px",
|
|
329
|
+
overflowY: "auto",
|
|
330
|
+
whiteSpace: "pre-wrap",
|
|
331
|
+
wordBreak: "break-all",
|
|
332
|
+
}}
|
|
333
|
+
>
|
|
334
|
+
{details.code && (
|
|
335
|
+
<div>
|
|
336
|
+
<strong>Code:</strong> {details.code}
|
|
337
|
+
</div>
|
|
338
|
+
)}
|
|
339
|
+
{details.originalMessage && (
|
|
340
|
+
<div style={{ marginTop: "4px" }}>
|
|
341
|
+
<strong>Message:</strong> {details.originalMessage}
|
|
342
|
+
</div>
|
|
343
|
+
)}
|
|
344
|
+
{details.context && Object.keys(details.context).length > 0 && (
|
|
345
|
+
<div style={{ marginTop: "4px" }}>
|
|
346
|
+
<strong>Context:</strong>{" "}
|
|
347
|
+
{JSON.stringify(details.context, null, 2)}
|
|
348
|
+
</div>
|
|
349
|
+
)}
|
|
350
|
+
{details.stack && (
|
|
351
|
+
<div style={{ marginTop: "4px", opacity: 0.7 }}>
|
|
352
|
+
<strong>Stack:</strong>
|
|
353
|
+
{"\n"}
|
|
354
|
+
{details.stack}
|
|
355
|
+
</div>
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
)}
|
|
359
|
+
</div>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
100
363
|
export function ToastProvider({
|
|
101
364
|
enabled,
|
|
102
365
|
children,
|
|
@@ -169,200 +432,12 @@ export function ToastProvider({
|
|
|
169
432
|
return (
|
|
170
433
|
<ToastContext.Provider value={value}>
|
|
171
434
|
{/* Banner Error Display */}
|
|
172
|
-
{bannerError &&
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
<div
|
|
179
|
-
style={{
|
|
180
|
-
position: "fixed",
|
|
181
|
-
bottom: "20px",
|
|
182
|
-
left: "50%",
|
|
183
|
-
transform: "translateX(-50%)",
|
|
184
|
-
zIndex: 9999,
|
|
185
|
-
backgroundColor: colors.background,
|
|
186
|
-
border: `1px solid ${colors.border}`,
|
|
187
|
-
borderLeft: `4px solid ${colors.border}`,
|
|
188
|
-
borderRadius: "8px",
|
|
189
|
-
padding: "12px 16px",
|
|
190
|
-
fontSize: "13px",
|
|
191
|
-
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
|
|
192
|
-
backdropFilter: "blur(8px)",
|
|
193
|
-
maxWidth: "min(90vw, 700px)",
|
|
194
|
-
width: "100%",
|
|
195
|
-
boxSizing: "border-box",
|
|
196
|
-
overflow: "hidden",
|
|
197
|
-
}}
|
|
198
|
-
>
|
|
199
|
-
<div
|
|
200
|
-
style={{
|
|
201
|
-
display: "flex",
|
|
202
|
-
justifyContent: "space-between",
|
|
203
|
-
alignItems: "center",
|
|
204
|
-
gap: "10px",
|
|
205
|
-
}}
|
|
206
|
-
>
|
|
207
|
-
<div
|
|
208
|
-
style={{
|
|
209
|
-
display: "flex",
|
|
210
|
-
alignItems: "center",
|
|
211
|
-
gap: "8px",
|
|
212
|
-
flex: 1,
|
|
213
|
-
minWidth: 0,
|
|
214
|
-
}}
|
|
215
|
-
>
|
|
216
|
-
<div
|
|
217
|
-
style={{
|
|
218
|
-
width: "12px",
|
|
219
|
-
height: "12px",
|
|
220
|
-
borderRadius: "50%",
|
|
221
|
-
backgroundColor: colors.border,
|
|
222
|
-
flexShrink: 0,
|
|
223
|
-
}}
|
|
224
|
-
/>
|
|
225
|
-
<div
|
|
226
|
-
style={{
|
|
227
|
-
display: "flex",
|
|
228
|
-
alignItems: "center",
|
|
229
|
-
gap: "10px",
|
|
230
|
-
flex: 1,
|
|
231
|
-
minWidth: 0,
|
|
232
|
-
}}
|
|
233
|
-
>
|
|
234
|
-
<div
|
|
235
|
-
style={{
|
|
236
|
-
color: colors.text,
|
|
237
|
-
lineHeight: "1.4",
|
|
238
|
-
fontWeight: "400",
|
|
239
|
-
fontSize: "13px",
|
|
240
|
-
flex: 1,
|
|
241
|
-
wordBreak: "break-all",
|
|
242
|
-
overflowWrap: "break-word",
|
|
243
|
-
maxWidth: "550px",
|
|
244
|
-
overflow: "hidden",
|
|
245
|
-
display: "-webkit-box",
|
|
246
|
-
WebkitLineClamp: 10,
|
|
247
|
-
WebkitBoxOrient: "vertical",
|
|
248
|
-
}}
|
|
249
|
-
>
|
|
250
|
-
{(() => {
|
|
251
|
-
let message = bannerError.message;
|
|
252
|
-
|
|
253
|
-
// Try to extract the useful message from JSON first
|
|
254
|
-
const jsonMatch = message.match(
|
|
255
|
-
/'message':\s*'([^']+)'/,
|
|
256
|
-
);
|
|
257
|
-
if (jsonMatch) {
|
|
258
|
-
return jsonMatch[1]; // Return the actual error message
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Strip technical garbage but keep the meaningful message
|
|
262
|
-
message = message.split(" - ")[0]; // Remove everything after " - {"
|
|
263
|
-
message = message.split(": Error code")[0]; // Remove ": Error code: 401"
|
|
264
|
-
message = message.replace(/:\s*\d{3}$/, ""); // Remove trailing ": 401"
|
|
265
|
-
message = message.replace(/See more:.*$/g, ""); // Remove "See more" links
|
|
266
|
-
message = message.trim();
|
|
267
|
-
|
|
268
|
-
// If it's still garbage (contains { or '), use fallback
|
|
269
|
-
// if (message.includes("{") || message.includes("'")) {
|
|
270
|
-
// return "Configuration error.... Please check your setup.";
|
|
271
|
-
// }
|
|
272
|
-
|
|
273
|
-
return message || "Configuration error occurred.";
|
|
274
|
-
})()}
|
|
275
|
-
</div>
|
|
276
|
-
|
|
277
|
-
{(() => {
|
|
278
|
-
const message = bannerError.message;
|
|
279
|
-
const markdownLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
280
|
-
const plainUrlRegex = /(https?:\/\/[^\s)]+)/g;
|
|
281
|
-
|
|
282
|
-
// Extract the first URL found
|
|
283
|
-
let url = null;
|
|
284
|
-
let buttonText = "See More";
|
|
285
|
-
|
|
286
|
-
// Check for markdown links first
|
|
287
|
-
const markdownMatch = markdownLinkRegex.exec(message);
|
|
288
|
-
if (markdownMatch) {
|
|
289
|
-
url = markdownMatch[2];
|
|
290
|
-
buttonText = "See More";
|
|
291
|
-
} else {
|
|
292
|
-
// Check for plain URLs
|
|
293
|
-
const urlMatch = plainUrlRegex.exec(message);
|
|
294
|
-
if (urlMatch) {
|
|
295
|
-
url = urlMatch[0].replace(/[.,;:'"]*$/, ""); // Remove trailing punctuation
|
|
296
|
-
buttonText = "See More";
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (!url) return null;
|
|
301
|
-
|
|
302
|
-
return (
|
|
303
|
-
<button
|
|
304
|
-
onClick={() =>
|
|
305
|
-
window.open(url, "_blank", "noopener,noreferrer")
|
|
306
|
-
}
|
|
307
|
-
style={{
|
|
308
|
-
background: colors.border,
|
|
309
|
-
color: "white",
|
|
310
|
-
border: "none",
|
|
311
|
-
borderRadius: "5px",
|
|
312
|
-
padding: "4px 10px",
|
|
313
|
-
fontSize: "11px",
|
|
314
|
-
fontWeight: "500",
|
|
315
|
-
cursor: "pointer",
|
|
316
|
-
transition: "all 0.2s ease",
|
|
317
|
-
flexShrink: 0,
|
|
318
|
-
}}
|
|
319
|
-
onMouseEnter={(e) => {
|
|
320
|
-
e.currentTarget.style.opacity = "0.9";
|
|
321
|
-
e.currentTarget.style.transform =
|
|
322
|
-
"translateY(-1px)";
|
|
323
|
-
}}
|
|
324
|
-
onMouseLeave={(e) => {
|
|
325
|
-
e.currentTarget.style.opacity = "1";
|
|
326
|
-
e.currentTarget.style.transform = "translateY(0)";
|
|
327
|
-
}}
|
|
328
|
-
>
|
|
329
|
-
{buttonText}
|
|
330
|
-
</button>
|
|
331
|
-
);
|
|
332
|
-
})()}
|
|
333
|
-
</div>
|
|
334
|
-
</div>
|
|
335
|
-
<button
|
|
336
|
-
onClick={() => setBannerError(null)}
|
|
337
|
-
style={{
|
|
338
|
-
background: "transparent",
|
|
339
|
-
border: "none",
|
|
340
|
-
color: colors.text,
|
|
341
|
-
cursor: "pointer",
|
|
342
|
-
padding: "2px",
|
|
343
|
-
borderRadius: "3px",
|
|
344
|
-
fontSize: "14px",
|
|
345
|
-
lineHeight: "1",
|
|
346
|
-
opacity: 0.6,
|
|
347
|
-
transition: "all 0.2s ease",
|
|
348
|
-
flexShrink: 0,
|
|
349
|
-
}}
|
|
350
|
-
title="Dismiss"
|
|
351
|
-
onMouseEnter={(e) => {
|
|
352
|
-
e.currentTarget.style.opacity = "1";
|
|
353
|
-
e.currentTarget.style.background = "rgba(0, 0, 0, 0.05)";
|
|
354
|
-
}}
|
|
355
|
-
onMouseLeave={(e) => {
|
|
356
|
-
e.currentTarget.style.opacity = "0.6";
|
|
357
|
-
e.currentTarget.style.background = "transparent";
|
|
358
|
-
}}
|
|
359
|
-
>
|
|
360
|
-
×
|
|
361
|
-
</button>
|
|
362
|
-
</div>
|
|
363
|
-
</div>
|
|
364
|
-
);
|
|
365
|
-
})()}
|
|
435
|
+
{bannerError && (
|
|
436
|
+
<BannerErrorDisplay
|
|
437
|
+
bannerError={bannerError}
|
|
438
|
+
onDismiss={() => setBannerError(null)}
|
|
439
|
+
/>
|
|
440
|
+
)}
|
|
366
441
|
|
|
367
442
|
{/* Toast Display - Deprecated: All errors now show as banners */}
|
|
368
443
|
{children}
|
|
@@ -178,18 +178,32 @@ describe("useCopilotChatInternal – connectAgent guard", () => {
|
|
|
178
178
|
});
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
-
it("does not call connectAgent when threadId matches agent
|
|
181
|
+
it("does not call connectAgent when threadId matches (same agent instance, no re-render)", async () => {
|
|
182
|
+
// useAgent now returns a per-thread clone, so the wrapper guards via
|
|
183
|
+
// lastConnectedAgentRef: connect fires once per agent instance, not once
|
|
184
|
+
// per render. After the first connect, further re-renders with the same
|
|
185
|
+
// agent do not trigger another connect.
|
|
182
186
|
mockRuntimeConnectionStatus =
|
|
183
187
|
CopilotKitCoreRuntimeConnectionStatus.Connected;
|
|
184
|
-
mockAgent.threadId = "config-thread-id";
|
|
188
|
+
mockAgent.threadId = "config-thread-id";
|
|
185
189
|
applyMocks();
|
|
186
190
|
|
|
187
|
-
renderHook(() => useCopilotChatInternal(), {
|
|
191
|
+
const { rerender } = renderHook(() => useCopilotChatInternal(), {
|
|
192
|
+
wrapper: createWrapper(),
|
|
193
|
+
});
|
|
188
194
|
|
|
189
|
-
|
|
195
|
+
await vi.waitFor(() => {
|
|
196
|
+
expect(mockConnectAgent).toHaveBeenCalledTimes(1);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Re-render with same agent — should NOT connect again
|
|
200
|
+
rerender();
|
|
201
|
+
await vi.waitFor(() => {
|
|
202
|
+
expect(mockConnectAgent).toHaveBeenCalledTimes(1);
|
|
203
|
+
});
|
|
190
204
|
});
|
|
191
205
|
|
|
192
|
-
it("
|
|
206
|
+
it("calls connectAgent when config threadId is missing", async () => {
|
|
193
207
|
mockRuntimeConnectionStatus =
|
|
194
208
|
CopilotKitCoreRuntimeConnectionStatus.Connected;
|
|
195
209
|
mockConfigThreadId = undefined;
|
|
@@ -197,10 +211,12 @@ describe("useCopilotChatInternal – connectAgent guard", () => {
|
|
|
197
211
|
|
|
198
212
|
renderHook(() => useCopilotChatInternal(), { wrapper: createWrapper() });
|
|
199
213
|
|
|
200
|
-
|
|
214
|
+
await vi.waitFor(() => {
|
|
215
|
+
expect(mockConnectAgent).toHaveBeenCalledTimes(1);
|
|
216
|
+
});
|
|
201
217
|
});
|
|
202
218
|
|
|
203
|
-
it("calls connectAgent when
|
|
219
|
+
it("calls connectAgent when status is Connected and threadIds differ", async () => {
|
|
204
220
|
mockRuntimeConnectionStatus =
|
|
205
221
|
CopilotKitCoreRuntimeConnectionStatus.Connected;
|
|
206
222
|
mockAgent.threadId = "old-thread-id"; // differs from config
|
|
@@ -214,18 +230,13 @@ describe("useCopilotChatInternal – connectAgent guard", () => {
|
|
|
214
230
|
});
|
|
215
231
|
});
|
|
216
232
|
|
|
217
|
-
it("
|
|
218
|
-
mockRuntimeConnectionStatus =
|
|
219
|
-
CopilotKitCoreRuntimeConnectionStatus.Connected;
|
|
220
|
-
mockAgent.threadId = "old-thread-id";
|
|
233
|
+
it("passes config threadId to useAgent", () => {
|
|
221
234
|
applyMocks();
|
|
222
235
|
|
|
223
236
|
renderHook(() => useCopilotChatInternal(), { wrapper: createWrapper() });
|
|
224
237
|
|
|
225
|
-
|
|
226
|
-
expect
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
expect(mockAgent.threadId).toBe("config-thread-id");
|
|
238
|
+
expect(vi.mocked(useAgent)).toHaveBeenCalledWith(
|
|
239
|
+
expect.objectContaining({ threadId: "config-thread-id" }),
|
|
240
|
+
);
|
|
230
241
|
});
|
|
231
242
|
});
|
|
@@ -337,7 +337,17 @@ export function useCopilotChatInternal({
|
|
|
337
337
|
|
|
338
338
|
// Apply priority: props > existing config > defaults
|
|
339
339
|
const resolvedAgentId = existingConfig?.agentId ?? "default";
|
|
340
|
-
const { agent } = useAgent({
|
|
340
|
+
const { agent } = useAgent({
|
|
341
|
+
agentId: resolvedAgentId,
|
|
342
|
+
threadId: existingConfig?.threadId,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Track the last agent instance we called connect() on. Without this,
|
|
346
|
+
// connect() fires on every render where status is Connected — including
|
|
347
|
+
// unrelated context re-renders and StrictMode double-invocations.
|
|
348
|
+
// The ref is reset in the cleanup so that remounts (StrictMode, real
|
|
349
|
+
// unmount/remount) always trigger a fresh connect.
|
|
350
|
+
const lastConnectedAgentRef = useRef<AbstractAgent | null>(null);
|
|
341
351
|
|
|
342
352
|
useEffect(() => {
|
|
343
353
|
let detached = false;
|
|
@@ -372,18 +382,19 @@ export function useCopilotChatInternal({
|
|
|
372
382
|
};
|
|
373
383
|
if (
|
|
374
384
|
agent &&
|
|
375
|
-
|
|
376
|
-
agent.threadId !== existingConfig.threadId &&
|
|
385
|
+
agent !== lastConnectedAgentRef.current &&
|
|
377
386
|
copilotkit.runtimeConnectionStatus ===
|
|
378
387
|
CopilotKitCoreRuntimeConnectionStatus.Connected
|
|
379
388
|
) {
|
|
380
|
-
|
|
389
|
+
lastConnectedAgentRef.current = agent;
|
|
381
390
|
connect(agent);
|
|
382
391
|
}
|
|
383
392
|
return () => {
|
|
384
393
|
// Abort the HTTP request and detach the active run.
|
|
385
394
|
// This is critical for React StrictMode which unmounts+remounts in dev,
|
|
386
395
|
// preventing duplicate /connect requests from reaching the server.
|
|
396
|
+
// Reset the ref so remounts always trigger a fresh connect.
|
|
397
|
+
lastConnectedAgentRef.current = null;
|
|
387
398
|
detached = true;
|
|
388
399
|
connectAbortController.abort();
|
|
389
400
|
agent?.detachActiveRun();
|