@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.
Files changed (94) hide show
  1. package/CHANGELOG.md +48 -5
  2. package/dist/{copilotkit-DNYSFuz5.mjs → copilotkit-BY5S1-0P.mjs} +2772 -858
  3. package/dist/copilotkit-BY5S1-0P.mjs.map +1 -0
  4. package/dist/{copilotkit-Dy5w3qEV.d.mts → copilotkit-BuhSUZHb.d.mts} +230 -17
  5. package/dist/copilotkit-BuhSUZHb.d.mts.map +1 -0
  6. package/dist/{copilotkit-B3Mb1yVE.cjs → copilotkit-Bz5-ImDl.cjs} +2776 -832
  7. package/dist/copilotkit-Bz5-ImDl.cjs.map +1 -0
  8. package/dist/{copilotkit-DBzgOMby.d.cts → copilotkit-dwDWYpya.d.cts} +230 -17
  9. package/dist/copilotkit-dwDWYpya.d.cts.map +1 -0
  10. package/dist/index.cjs +9 -4
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +1 -1
  13. package/dist/index.d.mts +1 -1
  14. package/dist/index.mjs +9 -4
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/index.umd.js +1624 -396
  17. package/dist/index.umd.js.map +1 -1
  18. package/dist/v2/index.cjs +13 -1
  19. package/dist/v2/index.css +1 -1
  20. package/dist/v2/index.d.cts +3 -3
  21. package/dist/v2/index.d.mts +3 -3
  22. package/dist/v2/index.mjs +3 -2
  23. package/dist/v2/index.umd.js +2746 -790
  24. package/dist/v2/index.umd.js.map +1 -1
  25. package/package.json +62 -54
  26. package/scripts/scope-preflight.mjs +1 -2
  27. package/src/components/CopilotListeners.tsx +41 -8
  28. package/src/components/copilot-provider/__tests__/copilot-messages-key.test.tsx +92 -0
  29. package/src/components/copilot-provider/copilotkit-props.tsx +4 -2
  30. package/src/components/copilot-provider/copilotkit.tsx +3 -3
  31. package/src/components/toast/toast-provider.tsx +269 -194
  32. package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +27 -16
  33. package/src/hooks/use-copilot-chat_internal.ts +15 -4
  34. package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +86 -22
  35. package/src/v2/__tests__/utils/test-helpers.tsx +107 -7
  36. package/src/v2/a2ui/A2UICatalogContext.tsx +79 -0
  37. package/src/v2/a2ui/A2UIMessageRenderer.tsx +125 -37
  38. package/src/v2/a2ui/A2UIToolCallRenderer.tsx +290 -0
  39. package/src/v2/components/CopilotKitInspector.tsx +2 -0
  40. package/src/v2/components/OpenGenerativeUIRenderer.tsx +598 -0
  41. package/src/v2/components/__tests__/OpenGenerativeUIRenderer.test.tsx +665 -0
  42. package/src/v2/components/chat/CopilotChat.tsx +197 -52
  43. package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +17 -2
  44. package/src/v2/components/chat/CopilotChatAttachmentQueue.tsx +481 -0
  45. package/src/v2/components/chat/CopilotChatAttachmentRenderer.tsx +139 -0
  46. package/src/v2/components/chat/CopilotChatInput.tsx +146 -77
  47. package/src/v2/components/chat/CopilotChatMessageView.tsx +260 -151
  48. package/src/v2/components/chat/CopilotChatSuggestionView.tsx +1 -0
  49. package/src/v2/components/chat/CopilotChatUserMessage.tsx +54 -0
  50. package/src/v2/components/chat/CopilotChatView.tsx +179 -66
  51. package/src/v2/components/chat/__tests__/CopilotChat.attachments.test.tsx +168 -0
  52. package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +63 -2
  53. package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +544 -1
  54. package/src/v2/components/chat/__tests__/CopilotChatPerf.e2e.test.tsx +268 -0
  55. package/src/v2/components/chat/__tests__/CopilotChatPropsRerender.e2e.test.tsx +249 -0
  56. package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.tsx +5 -2
  57. package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +5 -2
  58. package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +60 -3
  59. package/src/v2/components/chat/__tests__/copilot-chat-throttle.test.tsx +138 -0
  60. package/src/v2/components/chat/index.ts +9 -0
  61. package/src/v2/components/chat/scroll-element-context.ts +13 -0
  62. package/src/v2/hooks/__tests__/use-agent-context-timing.e2e.test.tsx +8 -0
  63. package/src/v2/hooks/__tests__/use-agent-thread-isolation.test.tsx +327 -0
  64. package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +1003 -0
  65. package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +13 -2
  66. package/src/v2/hooks/__tests__/use-attachments.test.tsx +169 -0
  67. package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +23 -4
  68. package/src/v2/hooks/__tests__/use-threads.test.tsx +54 -0
  69. package/src/v2/hooks/index.ts +5 -0
  70. package/src/v2/hooks/use-agent.tsx +220 -15
  71. package/src/v2/hooks/use-attachments.tsx +269 -0
  72. package/src/v2/hooks/use-frontend-tool.tsx +5 -2
  73. package/src/v2/hooks/use-render-activity-message.tsx +9 -2
  74. package/src/v2/hooks/use-render-custom-messages.tsx +6 -1
  75. package/src/v2/hooks/use-threads.tsx +35 -15
  76. package/src/v2/index.ts +5 -1
  77. package/src/v2/lib/__tests__/processPartialHtml.test.ts +112 -0
  78. package/src/v2/lib/__tests__/slots.test.ts +56 -0
  79. package/src/v2/lib/processPartialHtml.ts +45 -0
  80. package/src/v2/lib/slots.tsx +42 -1
  81. package/src/v2/providers/CopilotChatConfigurationProvider.tsx +9 -3
  82. package/src/v2/providers/CopilotKitProvider.tsx +268 -32
  83. package/src/v2/providers/SandboxFunctionsContext.ts +10 -0
  84. package/src/v2/providers/__tests__/CopilotKitProvider.sandboxFunctions.test.tsx +198 -0
  85. package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +71 -0
  86. package/src/v2/providers/index.ts +7 -0
  87. package/src/v2/styles/globals.css +2 -1
  88. package/src/v2/types/index.ts +1 -0
  89. package/src/v2/types/sandbox-function.ts +11 -0
  90. package/dist/copilotkit-B3Mb1yVE.cjs.map +0 -1
  91. package/dist/copilotkit-DBzgOMby.d.cts.map +0 -1
  92. package/dist/copilotkit-DNYSFuz5.mjs.map +0 -1
  93. package/dist/copilotkit-Dy5w3qEV.d.mts.map +0 -1
  94. package/src/v2/components/__tests__/license-warning-banner.test.tsx +0 -46
@@ -1,4 +1,5 @@
1
1
  import { useAgent } from "../../hooks/use-agent";
2
+ import { useAttachments } from "../../hooks/use-attachments";
2
3
  import { useSuggestions } from "../../hooks/use-suggestions";
3
4
  import { CopilotChatView, CopilotChatViewProps } from "./CopilotChatView";
4
5
  import { CopilotChatInputMode } from "./CopilotChatInput";
@@ -12,16 +13,22 @@ import {
12
13
  randomUUID,
13
14
  TranscriptionErrorCode,
14
15
  } from "@copilotkit/shared";
16
+ import type { AttachmentsConfig, InputContent } from "@copilotkit/shared";
15
17
  import { Suggestion, CopilotKitCoreErrorCode } from "@copilotkit/core";
16
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
17
- import { merge } from "ts-deepmerge";
18
+ import React, {
19
+ useCallback,
20
+ useEffect,
21
+ useMemo,
22
+ useRef,
23
+ useState,
24
+ } from "react";
18
25
  import {
19
26
  useCopilotKit,
20
27
  useLicenseContext,
21
28
  } from "../../providers/CopilotKitProvider";
22
29
  import { InlineFeatureWarning } from "../../components/license-warning-banner";
23
30
  import { AbstractAgent, HttpAgent } from "@ag-ui/client";
24
- import { renderSlot, SlotValue } from "../../lib/slots";
31
+ import { renderSlot, useShallowStableRef, SlotValue } from "../../lib/slots";
25
32
  import {
26
33
  transcribeAudio,
27
34
  TranscriptionError,
@@ -34,12 +41,22 @@ export type CopilotChatProps = Omit<
34
41
  | "suggestions"
35
42
  | "suggestionLoadingIndexes"
36
43
  | "onSelectSuggestion"
44
+ // Attachment state props — managed internally based on `attachments` config
45
+ | "attachments"
46
+ | "onRemoveAttachment"
47
+ | "onAddFile"
48
+ | "dragOver"
49
+ | "onDragOver"
50
+ | "onDragLeave"
51
+ | "onDrop"
37
52
  > & {
38
53
  agentId?: string;
39
54
  threadId?: string;
40
55
  labels?: Partial<CopilotChatLabels>;
41
56
  chatView?: SlotValue<typeof CopilotChatView>;
42
57
  isModalDefaultOpen?: boolean;
58
+ /** Enable multimodal file attachments (images, audio, video, documents). */
59
+ attachments?: AttachmentsConfig;
43
60
  /**
44
61
  * Error handler scoped to this chat's agent. Fires in addition to the
45
62
  * provider-level onError (does not suppress it). Receives only errors
@@ -50,6 +67,18 @@ export type CopilotChatProps = Omit<
50
67
  code: CopilotKitCoreErrorCode;
51
68
  context: Record<string, any>;
52
69
  }) => void | Promise<void>;
70
+ /**
71
+ * Throttle interval (in milliseconds) for re-renders triggered by message
72
+ * change notifications. Overrides the provider-level `defaultThrottleMs`
73
+ * for this chat instance. Forwarded to the internal `useAgent()` hook,
74
+ * which resolves the effective throttle value.
75
+ *
76
+ * @default undefined — inherits from provider `defaultThrottleMs`;
77
+ * if that is also unset, re-renders are unthrottled. Note: passing
78
+ * `throttleMs={0}` explicitly disables throttling for this instance
79
+ * even when the provider specifies a non-zero `defaultThrottleMs`.
80
+ */
81
+ throttleMs?: number;
53
82
  };
54
83
  export function CopilotChat({
55
84
  agentId,
@@ -57,7 +86,9 @@ export function CopilotChat({
57
86
  labels,
58
87
  chatView,
59
88
  isModalDefaultOpen,
89
+ attachments: attachmentsConfig,
60
90
  onError,
91
+ throttleMs,
61
92
  ...props
62
93
  }: CopilotChatProps) {
63
94
  // Check for existing configuration provider
@@ -71,7 +102,11 @@ export function CopilotChat({
71
102
  [threadId, existingConfig?.threadId],
72
103
  );
73
104
 
74
- const { agent } = useAgent({ agentId: resolvedAgentId });
105
+ const { agent } = useAgent({
106
+ agentId: resolvedAgentId,
107
+ threadId: resolvedThreadId,
108
+ throttleMs,
109
+ });
75
110
  const { copilotkit } = useCopilotKit();
76
111
  const { suggestions: autoSuggestions } = useSuggestions({
77
112
  agentId: resolvedAgentId,
@@ -127,6 +162,21 @@ export function CopilotChat({
127
162
  );
128
163
  const [isTranscribing, setIsTranscribing] = useState(false);
129
164
 
165
+ // Attachments
166
+ const {
167
+ attachments: selectedAttachments,
168
+ enabled: attachmentsEnabled,
169
+ dragOver,
170
+ fileInputRef,
171
+ containerRef: chatContainerRef,
172
+ handleFileUpload,
173
+ handleDragOver,
174
+ handleDragLeave,
175
+ handleDrop,
176
+ removeAttachment,
177
+ consumeAttachments,
178
+ } = useAttachments({ config: attachmentsConfig });
179
+
130
180
  // Check if transcription is enabled
131
181
  const isTranscriptionEnabled = copilotkit.audioFileTranscriptionEnabled;
132
182
 
@@ -164,7 +214,6 @@ export function CopilotChat({
164
214
  console.error("CopilotChat: connectAgent failed", error);
165
215
  }
166
216
  };
167
- agent.threadId = resolvedThreadId;
168
217
  connect(agent);
169
218
  return () => {
170
219
  // Abort the HTTP request and detach the active run.
@@ -184,11 +233,47 @@ export function CopilotChat({
184
233
 
185
234
  const onSubmitInput = useCallback(
186
235
  async (value: string) => {
187
- agent.addMessage({
188
- id: randomUUID(),
189
- role: "user",
190
- content: value,
191
- });
236
+ // Block if uploads in progress
237
+ const hasUploading = selectedAttachments.some(
238
+ (a) => a.status === "uploading",
239
+ );
240
+ if (hasUploading) {
241
+ console.error(
242
+ "[CopilotKit] Cannot send while attachments are uploading",
243
+ );
244
+ return;
245
+ }
246
+
247
+ const readyAttachments = consumeAttachments();
248
+
249
+ if (readyAttachments.length > 0) {
250
+ const contentParts: InputContent[] = [];
251
+ if (value.trim()) {
252
+ contentParts.push({ type: "text", text: value });
253
+ }
254
+ for (const att of readyAttachments) {
255
+ contentParts.push({
256
+ type: att.type,
257
+ source: att.source,
258
+ metadata: {
259
+ ...(att.filename ? { filename: att.filename } : {}),
260
+ ...att.metadata,
261
+ },
262
+ } as InputContent);
263
+ }
264
+ agent.addMessage({
265
+ id: randomUUID(),
266
+ role: "user",
267
+ content: contentParts,
268
+ });
269
+ } else {
270
+ agent.addMessage({
271
+ id: randomUUID(),
272
+ role: "user",
273
+ content: value,
274
+ });
275
+ }
276
+
192
277
  // Clear input after submitting
193
278
  setInputValue("");
194
279
  try {
@@ -199,7 +284,7 @@ export function CopilotChat({
199
284
  },
200
285
  // copilotkit is intentionally excluded — it is a stable ref that never changes.
201
286
  // eslint-disable-next-line react-hooks/exhaustive-deps
202
- [agent],
287
+ [agent, selectedAttachments, consumeAttachments],
203
288
  );
204
289
 
205
290
  const handleSelectSuggestion = useCallback(
@@ -334,22 +419,38 @@ export function CopilotChat({
334
419
  }
335
420
  }, [transcriptionError]);
336
421
 
337
- const mergedProps = merge(
338
- {
339
- isRunning: agent.isRunning,
340
- suggestions: autoSuggestions,
341
- onSelectSuggestion: handleSelectSuggestion,
342
- suggestionView: providedSuggestionView,
343
- },
344
- {
345
- ...restProps,
346
- ...(typeof providedMessageView === "string"
347
- ? { messageView: { className: providedMessageView } }
348
- : providedMessageView !== undefined
349
- ? { messageView: providedMessageView }
350
- : {}),
351
- },
422
+ // Stabilize slot object references so inline props (new object reference on
423
+ // every parent render) don't defeat MemoizedSlotWrapper's shallow equality
424
+ // check and cause unnecessary re-renders of the message list on each keystroke.
425
+ const stableMessageView = useShallowStableRef(
426
+ typeof providedMessageView === "string"
427
+ ? { className: providedMessageView }
428
+ : providedMessageView,
352
429
  );
430
+ const stableSuggestionView = useShallowStableRef(providedSuggestionView);
431
+
432
+ // Stabilize the `onAddFile` handler. Without useCallback, a new arrow
433
+ // function is created inline on every render, causing CopilotChatView to
434
+ // re-render on every keystroke even when nothing else changed.
435
+ const handleAddFile = useCallback(() => {
436
+ // Delay to let Radix dropdown menu close before triggering file input
437
+ setTimeout(() => {
438
+ fileInputRef.current?.click();
439
+ }, 100);
440
+ }, []);
441
+
442
+ // Use shallow spread instead of ts-deepmerge. ts-deepmerge deep-clones plain
443
+ // objects even from a single source, which would defeat the reference
444
+ // stability we just established for stableMessageView and other slot values.
445
+ const mergedProps: Partial<CopilotChatViewProps> = {
446
+ isRunning: agent.isRunning,
447
+ suggestions: autoSuggestions,
448
+ onSelectSuggestion: handleSelectSuggestion,
449
+ suggestionView: stableSuggestionView,
450
+ ...restProps,
451
+ };
452
+ if (stableMessageView !== undefined)
453
+ mergedProps.messageView = stableMessageView;
353
454
 
354
455
  const hasMessages = agent.messages.length > 0;
355
456
  const shouldAllowStop = agent.isRunning && hasMessages;
@@ -365,15 +466,39 @@ export function CopilotChat({
365
466
  ? "processing"
366
467
  : transcribeMode;
367
468
 
368
- // Memoize messages array - only create new reference when content actually changes
369
- // (agent.messages is mutated in place, so we need a new reference for React to detect changes)
370
-
469
+ // Memoize messages array only create a new reference when content changes.
470
+ // We build a lightweight fingerprint instead of JSON.stringify to avoid
471
+ // serializing large base64 attachment data on every render. The key captures:
472
+ // - message id, role, content length (text streaming)
473
+ // - content part count (multimodal additions)
474
+ // - tool call ids + argument lengths (tool call streaming)
475
+ const messagesMemoKey = agent.messages
476
+ .map((m) => {
477
+ const contentKey =
478
+ typeof m.content === "string"
479
+ ? m.content.length
480
+ : Array.isArray(m.content)
481
+ ? m.content.length
482
+ : 0;
483
+ const toolCallsKey =
484
+ "toolCalls" in m && Array.isArray(m.toolCalls)
485
+ ? m.toolCalls
486
+ .map(
487
+ (tc: any) => `${tc.id}:${tc.function?.arguments?.length ?? 0}`,
488
+ )
489
+ .join(";")
490
+ : "";
491
+ return `${m.id}:${m.role}:${contentKey}:${toolCallsKey}`;
492
+ })
493
+ .join(",");
371
494
  const messages = useMemo(
372
495
  () => [...agent.messages],
373
- [JSON.stringify(agent.messages)],
496
+ // eslint-disable-next-line react-hooks/exhaustive-deps
497
+ [messagesMemoKey],
374
498
  );
375
499
 
376
- const finalProps = merge(mergedProps, {
500
+ const finalProps: CopilotChatViewProps = {
501
+ ...mergedProps,
377
502
  messages,
378
503
  // Input behavior props
379
504
  onSubmitMessage: onSubmitInput,
@@ -388,7 +513,15 @@ export function CopilotChat({
388
513
  onFinishTranscribeWithAudio: showTranscription
389
514
  ? handleFinishTranscribeWithAudio
390
515
  : undefined,
391
- }) as CopilotChatViewProps;
516
+ // Attachment props
517
+ attachments: selectedAttachments,
518
+ onRemoveAttachment: removeAttachment,
519
+ onAddFile: attachmentsEnabled ? handleAddFile : undefined,
520
+ dragOver,
521
+ onDragOver: handleDragOver,
522
+ onDragLeave: handleDragLeave,
523
+ onDrop: handleDrop,
524
+ };
392
525
 
393
526
  // Always create a provider with merged values
394
527
  // This ensures priority: props > existing config > defaults
@@ -401,26 +534,38 @@ export function CopilotChat({
401
534
  labels={labels}
402
535
  isModalDefaultOpen={isModalDefaultOpen}
403
536
  >
404
- {!isChatLicensed && <InlineFeatureWarning featureName="Chat" />}
405
- {transcriptionError && (
406
- <div
407
- style={{
408
- position: "absolute",
409
- bottom: "100px",
410
- left: "50%",
411
- transform: "translateX(-50%)",
412
- backgroundColor: "#ef4444",
413
- color: "white",
414
- padding: "8px 16px",
415
- borderRadius: "8px",
416
- fontSize: "14px",
417
- zIndex: 50,
418
- }}
419
- >
420
- {transcriptionError}
421
- </div>
422
- )}
423
- {RenderedChatView}
537
+ <div ref={chatContainerRef} style={{ display: "contents" }}>
538
+ {attachmentsEnabled && (
539
+ <input
540
+ type="file"
541
+ multiple
542
+ ref={fileInputRef}
543
+ onChange={handleFileUpload}
544
+ accept={attachmentsConfig?.accept ?? "*/*"}
545
+ style={{ display: "none" }}
546
+ />
547
+ )}
548
+ {!isChatLicensed && <InlineFeatureWarning featureName="Chat" />}
549
+ {transcriptionError && (
550
+ <div
551
+ style={{
552
+ position: "absolute",
553
+ bottom: "100px",
554
+ left: "50%",
555
+ transform: "translateX(-50%)",
556
+ backgroundColor: "#ef4444",
557
+ color: "white",
558
+ padding: "8px 16px",
559
+ borderRadius: "8px",
560
+ fontSize: "14px",
561
+ zIndex: 50,
562
+ }}
563
+ >
564
+ {transcriptionError}
565
+ </div>
566
+ )}
567
+ {RenderedChatView}
568
+ </div>
424
569
  </CopilotChatConfigurationProvider>
425
570
  );
426
571
  }
@@ -1,5 +1,5 @@
1
1
  import { AssistantMessage, Message } from "@ag-ui/core";
2
- import { useState } from "react";
2
+ import { useEffect, useRef, useState } from "react";
3
3
  import {
4
4
  Copy,
5
5
  Check,
@@ -265,10 +265,25 @@ export namespace CopilotChatAssistantMessage {
265
265
  const config = useCopilotChatConfiguration();
266
266
  const labels = config?.labels ?? CopilotChatDefaultLabels;
267
267
  const [copied, setCopied] = useState(false);
268
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
269
+
270
+ useEffect(() => {
271
+ return () => {
272
+ if (timerRef.current !== null) {
273
+ clearTimeout(timerRef.current);
274
+ }
275
+ };
276
+ }, []);
268
277
 
269
278
  const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
270
279
  setCopied(true);
271
- setTimeout(() => setCopied(false), 2000);
280
+ if (timerRef.current !== null) {
281
+ clearTimeout(timerRef.current);
282
+ }
283
+ timerRef.current = setTimeout(() => {
284
+ timerRef.current = null;
285
+ setCopied(false);
286
+ }, 2000);
272
287
 
273
288
  if (onClick) {
274
289
  onClick(event);