@copilotkit/react-core 1.55.0-next.9 → 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 (81) hide show
  1. package/CHANGELOG.md +36 -6
  2. package/dist/{copilotkit-DeOzjPsb.mjs → copilotkit-BY5S1-0P.mjs} +2402 -552
  3. package/dist/copilotkit-BY5S1-0P.mjs.map +1 -0
  4. package/dist/{copilotkit-BqcyhQjT.d.mts → copilotkit-BuhSUZHb.d.mts} +228 -17
  5. package/dist/copilotkit-BuhSUZHb.d.mts.map +1 -0
  6. package/dist/{copilotkit-BDNjFNmk.cjs → copilotkit-Bz5-ImDl.cjs} +2421 -541
  7. package/dist/copilotkit-Bz5-ImDl.cjs.map +1 -0
  8. package/dist/{copilotkit-l-IBF4Xp.d.cts → copilotkit-dwDWYpya.d.cts} +228 -17
  9. package/dist/copilotkit-dwDWYpya.d.cts.map +1 -0
  10. package/dist/index.cjs +1 -1
  11. package/dist/index.d.cts +1 -1
  12. package/dist/index.d.mts +1 -1
  13. package/dist/index.mjs +1 -1
  14. package/dist/index.umd.js +1400 -238
  15. package/dist/index.umd.js.map +1 -1
  16. package/dist/v2/index.cjs +13 -1
  17. package/dist/v2/index.css +1 -1
  18. package/dist/v2/index.d.cts +3 -3
  19. package/dist/v2/index.d.mts +3 -3
  20. package/dist/v2/index.mjs +3 -2
  21. package/dist/v2/index.umd.js +2442 -552
  22. package/dist/v2/index.umd.js.map +1 -1
  23. package/package.json +62 -54
  24. package/scripts/scope-preflight.mjs +1 -2
  25. package/src/components/CopilotListeners.tsx +41 -8
  26. package/src/components/copilot-provider/copilotkit-props.tsx +4 -2
  27. package/src/components/toast/toast-provider.tsx +269 -194
  28. package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +86 -22
  29. package/src/v2/__tests__/utils/test-helpers.tsx +67 -0
  30. package/src/v2/a2ui/A2UICatalogContext.tsx +79 -0
  31. package/src/v2/a2ui/A2UIMessageRenderer.tsx +125 -37
  32. package/src/v2/a2ui/A2UIToolCallRenderer.tsx +290 -0
  33. package/src/v2/components/CopilotKitInspector.tsx +2 -0
  34. package/src/v2/components/OpenGenerativeUIRenderer.tsx +598 -0
  35. package/src/v2/components/__tests__/OpenGenerativeUIRenderer.test.tsx +665 -0
  36. package/src/v2/components/chat/CopilotChat.tsx +193 -50
  37. package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +17 -2
  38. package/src/v2/components/chat/CopilotChatAttachmentQueue.tsx +481 -0
  39. package/src/v2/components/chat/CopilotChatAttachmentRenderer.tsx +139 -0
  40. package/src/v2/components/chat/CopilotChatInput.tsx +146 -77
  41. package/src/v2/components/chat/CopilotChatMessageView.tsx +253 -149
  42. package/src/v2/components/chat/CopilotChatSuggestionView.tsx +1 -0
  43. package/src/v2/components/chat/CopilotChatUserMessage.tsx +54 -0
  44. package/src/v2/components/chat/CopilotChatView.tsx +179 -66
  45. package/src/v2/components/chat/__tests__/CopilotChat.attachments.test.tsx +168 -0
  46. package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +63 -2
  47. package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +544 -1
  48. package/src/v2/components/chat/__tests__/CopilotChatPerf.e2e.test.tsx +268 -0
  49. package/src/v2/components/chat/__tests__/CopilotChatPropsRerender.e2e.test.tsx +249 -0
  50. package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +43 -2
  51. package/src/v2/components/chat/__tests__/copilot-chat-throttle.test.tsx +138 -0
  52. package/src/v2/components/chat/index.ts +9 -0
  53. package/src/v2/components/chat/scroll-element-context.ts +13 -0
  54. package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +1003 -0
  55. package/src/v2/hooks/__tests__/use-attachments.test.tsx +169 -0
  56. package/src/v2/hooks/__tests__/use-threads.test.tsx +54 -0
  57. package/src/v2/hooks/index.ts +5 -0
  58. package/src/v2/hooks/use-agent.tsx +95 -10
  59. package/src/v2/hooks/use-attachments.tsx +269 -0
  60. package/src/v2/hooks/use-frontend-tool.tsx +5 -2
  61. package/src/v2/hooks/use-render-activity-message.tsx +9 -2
  62. package/src/v2/hooks/use-threads.tsx +35 -15
  63. package/src/v2/index.ts +5 -1
  64. package/src/v2/lib/__tests__/processPartialHtml.test.ts +112 -0
  65. package/src/v2/lib/__tests__/slots.test.ts +56 -0
  66. package/src/v2/lib/processPartialHtml.ts +45 -0
  67. package/src/v2/lib/slots.tsx +42 -1
  68. package/src/v2/providers/CopilotChatConfigurationProvider.tsx +9 -3
  69. package/src/v2/providers/CopilotKitProvider.tsx +268 -32
  70. package/src/v2/providers/SandboxFunctionsContext.ts +10 -0
  71. package/src/v2/providers/__tests__/CopilotKitProvider.sandboxFunctions.test.tsx +198 -0
  72. package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +71 -0
  73. package/src/v2/providers/index.ts +7 -0
  74. package/src/v2/styles/globals.css +2 -1
  75. package/src/v2/types/index.ts +1 -0
  76. package/src/v2/types/sandbox-function.ts +11 -0
  77. package/dist/copilotkit-BDNjFNmk.cjs.map +0 -1
  78. package/dist/copilotkit-BqcyhQjT.d.mts.map +0 -1
  79. package/dist/copilotkit-DeOzjPsb.mjs.map +0 -1
  80. package/dist/copilotkit-l-IBF4Xp.d.cts.map +0 -1
  81. 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
@@ -74,6 +105,7 @@ export function CopilotChat({
74
105
  const { agent } = useAgent({
75
106
  agentId: resolvedAgentId,
76
107
  threadId: resolvedThreadId,
108
+ throttleMs,
77
109
  });
78
110
  const { copilotkit } = useCopilotKit();
79
111
  const { suggestions: autoSuggestions } = useSuggestions({
@@ -130,6 +162,21 @@ export function CopilotChat({
130
162
  );
131
163
  const [isTranscribing, setIsTranscribing] = useState(false);
132
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
+
133
180
  // Check if transcription is enabled
134
181
  const isTranscriptionEnabled = copilotkit.audioFileTranscriptionEnabled;
135
182
 
@@ -186,11 +233,47 @@ export function CopilotChat({
186
233
 
187
234
  const onSubmitInput = useCallback(
188
235
  async (value: string) => {
189
- agent.addMessage({
190
- id: randomUUID(),
191
- role: "user",
192
- content: value,
193
- });
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
+
194
277
  // Clear input after submitting
195
278
  setInputValue("");
196
279
  try {
@@ -201,7 +284,7 @@ export function CopilotChat({
201
284
  },
202
285
  // copilotkit is intentionally excluded — it is a stable ref that never changes.
203
286
  // eslint-disable-next-line react-hooks/exhaustive-deps
204
- [agent],
287
+ [agent, selectedAttachments, consumeAttachments],
205
288
  );
206
289
 
207
290
  const handleSelectSuggestion = useCallback(
@@ -336,22 +419,38 @@ export function CopilotChat({
336
419
  }
337
420
  }, [transcriptionError]);
338
421
 
339
- const mergedProps = merge(
340
- {
341
- isRunning: agent.isRunning,
342
- suggestions: autoSuggestions,
343
- onSelectSuggestion: handleSelectSuggestion,
344
- suggestionView: providedSuggestionView,
345
- },
346
- {
347
- ...restProps,
348
- ...(typeof providedMessageView === "string"
349
- ? { messageView: { className: providedMessageView } }
350
- : providedMessageView !== undefined
351
- ? { messageView: providedMessageView }
352
- : {}),
353
- },
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,
354
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;
355
454
 
356
455
  const hasMessages = agent.messages.length > 0;
357
456
  const shouldAllowStop = agent.isRunning && hasMessages;
@@ -367,15 +466,39 @@ export function CopilotChat({
367
466
  ? "processing"
368
467
  : transcribeMode;
369
468
 
370
- // Memoize messages array - only create new reference when content actually changes
371
- // (agent.messages is mutated in place, so we need a new reference for React to detect changes)
372
-
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(",");
373
494
  const messages = useMemo(
374
495
  () => [...agent.messages],
375
- [JSON.stringify(agent.messages)],
496
+ // eslint-disable-next-line react-hooks/exhaustive-deps
497
+ [messagesMemoKey],
376
498
  );
377
499
 
378
- const finalProps = merge(mergedProps, {
500
+ const finalProps: CopilotChatViewProps = {
501
+ ...mergedProps,
379
502
  messages,
380
503
  // Input behavior props
381
504
  onSubmitMessage: onSubmitInput,
@@ -390,7 +513,15 @@ export function CopilotChat({
390
513
  onFinishTranscribeWithAudio: showTranscription
391
514
  ? handleFinishTranscribeWithAudio
392
515
  : undefined,
393
- }) 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
+ };
394
525
 
395
526
  // Always create a provider with merged values
396
527
  // This ensures priority: props > existing config > defaults
@@ -403,26 +534,38 @@ export function CopilotChat({
403
534
  labels={labels}
404
535
  isModalDefaultOpen={isModalDefaultOpen}
405
536
  >
406
- {!isChatLicensed && <InlineFeatureWarning featureName="Chat" />}
407
- {transcriptionError && (
408
- <div
409
- style={{
410
- position: "absolute",
411
- bottom: "100px",
412
- left: "50%",
413
- transform: "translateX(-50%)",
414
- backgroundColor: "#ef4444",
415
- color: "white",
416
- padding: "8px 16px",
417
- borderRadius: "8px",
418
- fontSize: "14px",
419
- zIndex: 50,
420
- }}
421
- >
422
- {transcriptionError}
423
- </div>
424
- )}
425
- {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>
426
569
  </CopilotChatConfigurationProvider>
427
570
  );
428
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);