@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
@@ -0,0 +1,269 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from "react";
2
+ import {
3
+ randomUUID,
4
+ getModalityFromMimeType,
5
+ exceedsMaxSize,
6
+ readFileAsBase64,
7
+ generateVideoThumbnail,
8
+ matchesAcceptFilter,
9
+ formatFileSize,
10
+ } from "@copilotkit/shared";
11
+ import type { Attachment, AttachmentsConfig } from "@copilotkit/shared";
12
+
13
+ export interface UseAttachmentsProps {
14
+ config?: AttachmentsConfig;
15
+ }
16
+
17
+ export interface UseAttachmentsReturn {
18
+ /** Currently selected attachments (uploading + ready). */
19
+ attachments: Attachment[];
20
+ /** Whether attachments are enabled. */
21
+ enabled: boolean;
22
+ /** Whether the user is dragging a file over the drop zone. */
23
+ dragOver: boolean;
24
+ /** Ref for the hidden file input element. */
25
+ fileInputRef: React.RefObject<HTMLInputElement | null>;
26
+ /** Ref for the container element (used for scoped paste handling). */
27
+ containerRef: React.RefObject<HTMLDivElement | null>;
28
+ /** Process an array of files (validate, upload, add to state). */
29
+ processFiles: (files: File[]) => Promise<void>;
30
+ /** Handler for `<input type="file" onChange>`. */
31
+ handleFileUpload: (e: React.ChangeEvent<HTMLInputElement>) => Promise<void>;
32
+ /** Handler for `onDragOver` on the drop zone. */
33
+ handleDragOver: (e: React.DragEvent) => void;
34
+ /** Handler for `onDragLeave` on the drop zone. */
35
+ handleDragLeave: (e: React.DragEvent) => void;
36
+ /** Handler for `onDrop` on the drop zone. */
37
+ handleDrop: (e: React.DragEvent) => Promise<void>;
38
+ /** Remove an attachment by ID. */
39
+ removeAttachment: (id: string) => void;
40
+ /**
41
+ * Consume ready attachments and clear the queue.
42
+ * Returns the attachments that were ready; resets the file input.
43
+ * No-ops if the queue is already empty (no state update triggered).
44
+ */
45
+ consumeAttachments: () => Attachment[];
46
+ }
47
+
48
+ /**
49
+ * Hook that manages file attachment state — uploads, drag-and-drop, paste,
50
+ * and lifecycle. All returned callbacks are referentially stable across
51
+ * renders (via useCallback) to avoid destabilizing downstream memoization.
52
+ */
53
+ export function useAttachments({
54
+ config,
55
+ }: UseAttachmentsProps): UseAttachmentsReturn {
56
+ const enabled = config?.enabled ?? false;
57
+
58
+ const [attachments, setAttachments] = useState<Attachment[]>([]);
59
+ const [dragOver, setDragOver] = useState(false);
60
+ const fileInputRef = useRef<HTMLInputElement>(null);
61
+ const containerRef = useRef<HTMLDivElement>(null);
62
+
63
+ // Keep refs to the latest values so stable callbacks can read current
64
+ // state without appearing in dependency arrays.
65
+ const configRef = useRef(config);
66
+ configRef.current = config;
67
+ const attachmentsRef = useRef<Attachment[]>([]);
68
+ attachmentsRef.current = attachments;
69
+
70
+ // Stable processFiles — reads config from ref, never changes identity
71
+ const processFiles = useCallback(async (files: File[]) => {
72
+ const cfg = configRef.current;
73
+ const accept = cfg?.accept ?? "*/*";
74
+ const maxSize = cfg?.maxSize ?? 20 * 1024 * 1024;
75
+
76
+ const rejectedFiles = files.filter(
77
+ (file) => !matchesAcceptFilter(file, accept),
78
+ );
79
+ for (const file of rejectedFiles) {
80
+ cfg?.onUploadFailed?.({
81
+ reason: "invalid-type",
82
+ file,
83
+ message: `File "${file.name}" is not accepted. Supported types: ${accept}`,
84
+ });
85
+ }
86
+
87
+ const validFiles = files.filter((file) =>
88
+ matchesAcceptFilter(file, accept),
89
+ );
90
+
91
+ for (const file of validFiles) {
92
+ if (exceedsMaxSize(file, maxSize)) {
93
+ cfg?.onUploadFailed?.({
94
+ reason: "file-too-large",
95
+ file,
96
+ message: `File "${file.name}" exceeds the maximum size of ${formatFileSize(maxSize)}`,
97
+ });
98
+ continue;
99
+ }
100
+
101
+ const modality = getModalityFromMimeType(file.type);
102
+ const placeholderId = randomUUID();
103
+ const placeholder: Attachment = {
104
+ id: placeholderId,
105
+ type: modality,
106
+ source: { type: "data", value: "", mimeType: file.type },
107
+ filename: file.name,
108
+ size: file.size,
109
+ status: "uploading",
110
+ };
111
+
112
+ setAttachments((prev) => [...prev, placeholder]);
113
+
114
+ try {
115
+ let source: Attachment["source"];
116
+ let uploadMetadata: Record<string, unknown> | undefined;
117
+
118
+ if (cfg?.onUpload) {
119
+ const { metadata: meta, ...uploadSource } = await cfg.onUpload(file);
120
+ source = uploadSource;
121
+ uploadMetadata = meta;
122
+ } else {
123
+ const base64 = await readFileAsBase64(file);
124
+ source = { type: "data", value: base64, mimeType: file.type };
125
+ }
126
+
127
+ let thumbnail: string | undefined;
128
+ if (modality === "video") {
129
+ thumbnail = await generateVideoThumbnail(file);
130
+ }
131
+
132
+ setAttachments((prev) =>
133
+ prev.map((att) =>
134
+ att.id === placeholderId
135
+ ? {
136
+ ...att,
137
+ source,
138
+ status: "ready" as const,
139
+ thumbnail,
140
+ metadata: uploadMetadata,
141
+ }
142
+ : att,
143
+ ),
144
+ );
145
+ } catch (error) {
146
+ setAttachments((prev) =>
147
+ prev.filter((att) => att.id !== placeholderId),
148
+ );
149
+ console.error(`[CopilotKit] Failed to upload "${file.name}":`, error);
150
+ cfg?.onUploadFailed?.({
151
+ reason: "upload-failed",
152
+ file,
153
+ message:
154
+ error instanceof Error
155
+ ? error.message
156
+ : `Failed to upload "${file.name}"`,
157
+ });
158
+ }
159
+ }
160
+ }, []);
161
+
162
+ const handleFileUpload = useCallback(
163
+ async (e: React.ChangeEvent<HTMLInputElement>) => {
164
+ if (!e.target.files?.length) return;
165
+ try {
166
+ await processFiles(Array.from(e.target.files));
167
+ } catch (error) {
168
+ console.error("[CopilotKit] Upload error:", error);
169
+ }
170
+ },
171
+ [processFiles],
172
+ );
173
+
174
+ const handleDragOver = useCallback((e: React.DragEvent) => {
175
+ if (!configRef.current?.enabled) return;
176
+ e.preventDefault();
177
+ e.stopPropagation();
178
+ setDragOver(true);
179
+ }, []);
180
+
181
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
182
+ e.preventDefault();
183
+ e.stopPropagation();
184
+ setDragOver(false);
185
+ }, []);
186
+
187
+ const handleDrop = useCallback(
188
+ async (e: React.DragEvent) => {
189
+ e.preventDefault();
190
+ e.stopPropagation();
191
+ setDragOver(false);
192
+ if (!configRef.current?.enabled) return;
193
+
194
+ const files = Array.from(e.dataTransfer.files);
195
+ if (files.length > 0) {
196
+ try {
197
+ await processFiles(files);
198
+ } catch (error) {
199
+ console.error("[CopilotKit] Drop error:", error);
200
+ }
201
+ }
202
+ },
203
+ [processFiles],
204
+ );
205
+
206
+ // Clipboard paste handler — scoped to the container
207
+ useEffect(() => {
208
+ if (!enabled) return;
209
+
210
+ const handlePaste = async (e: ClipboardEvent) => {
211
+ const target = e.target as HTMLElement | null;
212
+ if (!target || !containerRef.current?.contains(target)) return;
213
+
214
+ const accept = configRef.current?.accept ?? "*/*";
215
+ const items = Array.from(e.clipboardData?.items || []);
216
+ const fileItems = items.filter(
217
+ (item) =>
218
+ item.kind === "file" &&
219
+ item.getAsFile() !== null &&
220
+ matchesAcceptFilter(item.getAsFile()!, accept),
221
+ );
222
+
223
+ if (fileItems.length === 0) return;
224
+ e.preventDefault();
225
+
226
+ const files = fileItems
227
+ .map((item) => item.getAsFile())
228
+ .filter((f): f is File => f !== null);
229
+
230
+ try {
231
+ await processFiles(files);
232
+ } catch (error) {
233
+ console.error("[CopilotKit] Paste error:", error);
234
+ }
235
+ };
236
+
237
+ document.addEventListener("paste", handlePaste);
238
+ return () => document.removeEventListener("paste", handlePaste);
239
+ }, [enabled, processFiles]);
240
+
241
+ const removeAttachment = useCallback((id: string) => {
242
+ setAttachments((prev) => prev.filter((a) => a.id !== id));
243
+ }, []);
244
+
245
+ const consumeAttachments = useCallback(() => {
246
+ const ready = attachmentsRef.current.filter((a) => a.status === "ready");
247
+ if (ready.length === 0) return ready;
248
+ setAttachments((prev) => prev.filter((a) => a.status !== "ready"));
249
+ if (fileInputRef.current) {
250
+ fileInputRef.current.value = "";
251
+ }
252
+ return ready;
253
+ }, []);
254
+
255
+ return {
256
+ attachments,
257
+ enabled,
258
+ dragOver,
259
+ fileInputRef,
260
+ containerRef,
261
+ processFiles,
262
+ handleFileUpload,
263
+ handleDragOver,
264
+ handleDragLeave,
265
+ handleDrop,
266
+ removeAttachment,
267
+ consumeAttachments,
268
+ };
269
+ }
@@ -22,8 +22,11 @@ export function useFrontendTool<
22
22
  }
23
23
  copilotkit.addTool(tool);
24
24
 
25
- // Register/override renderer by name and agentId through core
26
- if (tool.render && tool.parameters) {
25
+ // Register/override renderer by name and agentId through core.
26
+ // The render function is registered even when tool.parameters is
27
+ // undefined — tools like HITL confirm dialogs have no parameters
28
+ // but still need their UI rendered in the chat.
29
+ if (tool.render) {
27
30
  copilotkit.addHookRenderToolCall({
28
31
  name,
29
32
  args: tool.parameters,
@@ -3,6 +3,7 @@ import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
3
3
  import { useCopilotKit, useCopilotChatConfiguration } from "../providers";
4
4
  import { useCallback, useMemo } from "react";
5
5
  import { ReactActivityMessageRenderer } from "../types";
6
+ import { getThreadClone } from "./use-agent";
6
7
 
7
8
  export function useRenderActivityMessage() {
8
9
  const { copilotkit } = useCopilotKit();
@@ -51,7 +52,13 @@ export function useRenderActivityMessage() {
51
52
  }
52
53
 
53
54
  const Component = renderer.render;
54
- const agent = copilotkit.getAgent(agentId);
55
+ // Prefer the per-thread clone so that handleAction in ReactSurfaceHost
56
+ // calls runAgent on the same agent instance that CopilotChat renders from.
57
+ // Without this, button clicks accumulate messages on the registry agent
58
+ // while CopilotChat displays from the clone — responses appear to vanish.
59
+ const registryAgent = copilotkit.getAgent(agentId);
60
+ const agent =
61
+ getThreadClone(registryAgent, config?.threadId) ?? registryAgent;
55
62
 
56
63
  return (
57
64
  <Component
@@ -63,7 +70,7 @@ export function useRenderActivityMessage() {
63
70
  />
64
71
  );
65
72
  },
66
- [agentId, copilotkit, findRenderer],
73
+ [agentId, config?.threadId, copilotkit, findRenderer],
67
74
  );
68
75
 
69
76
  return useMemo(
@@ -1,4 +1,5 @@
1
1
  import { useCopilotChatConfiguration, useCopilotKit } from "../providers";
2
+ import { getThreadClone } from "./use-agent";
2
3
  import { ReactCustomMessageRendererPosition } from "../types/react-custom-message-renderer";
3
4
  import { Message } from "@ag-ui/core";
4
5
 
@@ -38,7 +39,11 @@ export function useRenderCustomMessages() {
38
39
  copilotkit.getRunIdForMessage(agentId, threadId, message.id) ??
39
40
  copilotkit.getRunIdsForThread(agentId, threadId).slice(-1)[0];
40
41
  const runId = resolvedRunId ?? `missing-run-id:${message.id}`;
41
- const agent = copilotkit.getAgent(agentId);
42
+ // Prefer the per-thread clone so that agent.messages reflects the actual
43
+ // conversation state (messages live on the clone, not the registry agent).
44
+ // Fall back to the registry agent when no clone exists (no threadId).
45
+ const registryAgent = copilotkit.getAgent(agentId);
46
+ const agent = getThreadClone(registryAgent, threadId) ?? registryAgent;
42
47
  if (!agent) {
43
48
  throw new Error("Agent not found");
44
49
  }
@@ -6,7 +6,6 @@ import {
6
6
  ɵselectThreadsIsLoading,
7
7
  ɵselectHasNextPage,
8
8
  ɵselectIsFetchingNextPage,
9
- type ɵThread as CoreThread,
10
9
  type ɵThreadRuntimeContext,
11
10
  type ɵThreadStore,
12
11
  } from "@copilotkit/core";
@@ -24,7 +23,14 @@ import {
24
23
  * Each thread has a unique `id`, an optional human-readable `name`, and
25
24
  * timestamp fields tracking creation and update times.
26
25
  */
27
- export interface Thread extends CoreThread {}
26
+ export interface Thread {
27
+ id: string;
28
+ agentId: string;
29
+ name: string | null;
30
+ archived: boolean;
31
+ createdAt: string;
32
+ updatedAt: string;
33
+ }
28
34
 
29
35
  /**
30
36
  * Configuration for the {@link useThreads} hook.
@@ -68,18 +74,18 @@ export interface UseThreadsResult {
68
74
  error: Error | null;
69
75
  /**
70
76
  * `true` when there are more threads available to fetch via
71
- * {@link fetchNextPage}. Only meaningful when `limit` is set.
77
+ * {@link fetchMoreThreads}. Only meaningful when `limit` is set.
72
78
  */
73
- hasNextPage: boolean;
79
+ hasMoreThreads: boolean;
74
80
  /**
75
81
  * `true` while a subsequent page of threads is being fetched.
76
82
  */
77
- isFetchingNextPage: boolean;
83
+ isFetchingMoreThreads: boolean;
78
84
  /**
79
- * Fetch the next page of threads. No-op when {@link hasNextPage} is
80
- * `false` or a page fetch is already in progress.
85
+ * Fetch the next page of threads. No-op when {@link hasMoreThreads} is
86
+ * `false` or a fetch is already in progress.
81
87
  */
82
- fetchNextPage: () => void;
88
+ fetchMoreThreads: () => void;
83
89
  /**
84
90
  * Rename a thread on the platform.
85
91
  * Resolves when the server confirms the update; rejects on failure.
@@ -169,11 +175,25 @@ export function useThreads({
169
175
  }),
170
176
  );
171
177
 
172
- const threads = useThreadStoreSelector(store, ɵselectThreads);
178
+ const coreThreads = useThreadStoreSelector(store, ɵselectThreads);
179
+ const threads: Thread[] = useMemo(
180
+ () =>
181
+ coreThreads.map(
182
+ ({ id, agentId, name, archived, createdAt, updatedAt }) => ({
183
+ id,
184
+ agentId,
185
+ name,
186
+ archived,
187
+ createdAt,
188
+ updatedAt,
189
+ }),
190
+ ),
191
+ [coreThreads],
192
+ );
173
193
  const storeIsLoading = useThreadStoreSelector(store, ɵselectThreadsIsLoading);
174
194
  const storeError = useThreadStoreSelector(store, ɵselectThreadsError);
175
- const hasNextPage = useThreadStoreSelector(store, ɵselectHasNextPage);
176
- const isFetchingNextPage = useThreadStoreSelector(
195
+ const hasMoreThreads = useThreadStoreSelector(store, ɵselectHasNextPage);
196
+ const isFetchingMoreThreads = useThreadStoreSelector(
177
197
  store,
178
198
  ɵselectIsFetchingNextPage,
179
199
  );
@@ -240,15 +260,15 @@ export function useThreads({
240
260
  [store],
241
261
  );
242
262
 
243
- const fetchNextPage = useCallback(() => store.fetchNextPage(), [store]);
263
+ const fetchMoreThreads = useCallback(() => store.fetchNextPage(), [store]);
244
264
 
245
265
  return {
246
266
  threads,
247
267
  isLoading,
248
268
  error,
249
- hasNextPage,
250
- isFetchingNextPage,
251
- fetchNextPage,
269
+ hasMoreThreads,
270
+ isFetchingMoreThreads,
271
+ fetchMoreThreads,
252
272
  renameThread,
253
273
  archiveThread,
254
274
  deleteThread,
package/src/v2/index.ts CHANGED
@@ -15,8 +15,12 @@ export * from "./providers";
15
15
  export * from "./types";
16
16
  export * from "./lib/react-core";
17
17
  export { createA2UIMessageRenderer } from "./a2ui/A2UIMessageRenderer";
18
- export type { A2UIMessageRendererOptions } from "./a2ui/A2UIMessageRenderer";
18
+ export type {
19
+ A2UIMessageRendererOptions,
20
+ A2UIUserAction,
21
+ } from "./a2ui/A2UIMessageRenderer";
19
22
  export type { Theme as A2UITheme } from "@copilotkit/a2ui-renderer";
23
+ export { defaultTheme as a2uiDefaultTheme } from "@copilotkit/a2ui-renderer";
20
24
 
21
25
  // V1 backward-compat re-exports
22
26
  export { CopilotKit } from "../components/copilot-provider/copilotkit";
@@ -0,0 +1,112 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ processPartialHtml,
4
+ extractCompleteStyles,
5
+ } from "../processPartialHtml";
6
+
7
+ describe("processPartialHtml", () => {
8
+ it("returns empty string for empty input", () => {
9
+ expect(processPartialHtml("")).toBe("");
10
+ });
11
+
12
+ it("strips incomplete tag at end", () => {
13
+ expect(processPartialHtml('<div>Hello<span class="fo')).toBe("<div>Hello");
14
+ });
15
+
16
+ it("strips complete <style> blocks", () => {
17
+ const input =
18
+ "<div>Hello</div><style>.foo { color: red; }</style><p>World</p>";
19
+ expect(processPartialHtml(input)).toBe("<div>Hello</div><p>World</p>");
20
+ });
21
+
22
+ it("strips complete <script> blocks", () => {
23
+ const input = '<div>Hello</div><script>alert("hi")</script><p>World</p>';
24
+ expect(processPartialHtml(input)).toBe("<div>Hello</div><p>World</p>");
25
+ });
26
+
27
+ it("strips incomplete <style> block", () => {
28
+ const input = "<div>Hello</div><style>.foo { color:";
29
+ expect(processPartialHtml(input)).toBe("<div>Hello</div>");
30
+ });
31
+
32
+ it("strips incomplete <script> block", () => {
33
+ const input = '<div>Hello</div><script>const x = "val';
34
+ expect(processPartialHtml(input)).toBe("<div>Hello</div>");
35
+ });
36
+
37
+ it("strips incomplete HTML entities", () => {
38
+ expect(processPartialHtml("<p>Hello &amp")).toBe("<p>Hello ");
39
+ expect(processPartialHtml("<p>Hello &#123")).toBe("<p>Hello ");
40
+ });
41
+
42
+ it("preserves complete entities", () => {
43
+ expect(processPartialHtml("<p>Hello &amp; World</p>")).toBe(
44
+ "<p>Hello &amp; World</p>",
45
+ );
46
+ });
47
+
48
+ it("extracts body content from full HTML document", () => {
49
+ const input =
50
+ "<html><head><title>Test</title></head><body><p>Content</p></body></html>";
51
+ expect(processPartialHtml(input)).toBe("<p>Content</p>");
52
+ });
53
+
54
+ it("handles <body> with attributes", () => {
55
+ const input = '<body class="dark"><p>Content</p></body>';
56
+ expect(processPartialHtml(input)).toBe("<p>Content</p>");
57
+ });
58
+
59
+ it("handles no <body> tag — returns full processed string", () => {
60
+ const input = "<div><p>Just content</p></div>";
61
+ expect(processPartialHtml(input)).toBe("<div><p>Just content</p></div>");
62
+ });
63
+
64
+ it("handles combined edge cases: full document with styles, scripts, and incomplete tag", () => {
65
+ const input =
66
+ '<html><head><style>body { margin: 0; }</style></head><body><div>Hello</div><script>console.log("x")</script><p>World</p><span class="in';
67
+ expect(processPartialHtml(input)).toBe("<div>Hello</div><p>World</p>");
68
+ });
69
+
70
+ it("handles body content with incomplete style at end", () => {
71
+ const input = "<body><div>Content</div><style>.partial {";
72
+ expect(processPartialHtml(input)).toBe("<div>Content</div>");
73
+ });
74
+ });
75
+
76
+ describe("extractCompleteStyles", () => {
77
+ it("returns empty string for no styles", () => {
78
+ expect(extractCompleteStyles("<div>Hello</div>")).toBe("");
79
+ });
80
+
81
+ it("returns empty string for empty input", () => {
82
+ expect(extractCompleteStyles("")).toBe("");
83
+ });
84
+
85
+ it("extracts a single complete style block", () => {
86
+ const input =
87
+ "<div>Hello</div><style>.foo { color: red; }</style><p>World</p>";
88
+ expect(extractCompleteStyles(input)).toBe(
89
+ "<style>.foo { color: red; }</style>",
90
+ );
91
+ });
92
+
93
+ it("extracts multiple complete style blocks", () => {
94
+ const input = "<style>a{}</style><div>X</div><style>b{}</style>";
95
+ expect(extractCompleteStyles(input)).toBe(
96
+ "<style>a{}</style><style>b{}</style>",
97
+ );
98
+ });
99
+
100
+ it("ignores incomplete style blocks", () => {
101
+ const input = "<style>.complete{}</style><style>.incomplete {";
102
+ expect(extractCompleteStyles(input)).toBe("<style>.complete{}</style>");
103
+ });
104
+
105
+ it("extracts styles from head", () => {
106
+ const input =
107
+ "<head><style>body { margin: 0; }</style></head><body><p>Hi</p></body>";
108
+ expect(extractCompleteStyles(input)).toBe(
109
+ "<style>body { margin: 0; }</style>",
110
+ );
111
+ });
112
+ });
@@ -0,0 +1,56 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { describe, it, expect } from "vitest";
3
+ import { useShallowStableRef } from "../slots";
4
+
5
+ describe("useShallowStableRef", () => {
6
+ it("returns the same reference when called twice with shallowly equal plain objects", () => {
7
+ const initial = { a: 1 };
8
+ const { result, rerender } = renderHook(
9
+ ({ value }: { value: { a: number } }) => useShallowStableRef(value),
10
+ { initialProps: { value: initial } },
11
+ );
12
+
13
+ const firstRef = result.current;
14
+ rerender({ value: { a: 1 } }); // new object, same shape
15
+ expect(result.current).toBe(firstRef);
16
+ });
17
+
18
+ it("updates the reference when the value changes", () => {
19
+ const { result, rerender } = renderHook(
20
+ ({ value }: { value: { a: number } }) => useShallowStableRef(value),
21
+ { initialProps: { value: { a: 1 } } },
22
+ );
23
+
24
+ const firstRef = result.current;
25
+ rerender({ value: { a: 2 } });
26
+ expect(result.current).not.toBe(firstRef);
27
+ expect(result.current).toEqual({ a: 2 });
28
+ });
29
+
30
+ it("handles undefined without crashing", () => {
31
+ const { result } = renderHook(() =>
32
+ useShallowStableRef(undefined as unknown as { a: number }),
33
+ );
34
+ expect(result.current).toBeUndefined();
35
+ });
36
+
37
+ it("handles null without crashing", () => {
38
+ const { result } = renderHook(() =>
39
+ useShallowStableRef(null as unknown as { a: number }),
40
+ );
41
+ expect(result.current).toBeNull();
42
+ });
43
+
44
+ it("does not shallow-compare arrays — treats them by reference", () => {
45
+ const arr1 = [1, 2, 3];
46
+ const { result, rerender } = renderHook(
47
+ ({ value }: { value: number[] }) => useShallowStableRef(value),
48
+ { initialProps: { value: arr1 } },
49
+ );
50
+
51
+ const firstRef = result.current;
52
+ rerender({ value: [1, 2, 3] }); // new array, same contents
53
+ // arrays are not plain objects — reference should update
54
+ expect(result.current).not.toBe(firstRef);
55
+ });
56
+ });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Extracts all complete `<style>` blocks from the raw HTML.
3
+ * Returns the concatenated style tags, suitable for injection into `<head>`.
4
+ */
5
+ export function extractCompleteStyles(html: string): string {
6
+ const matches = html.match(/<style\b[^>]*>[\s\S]*?<\/style>/gi);
7
+ return matches ? matches.join("") : "";
8
+ }
9
+
10
+ /**
11
+ * Processes raw accumulated HTML for safe preview via innerHTML injection.
12
+ * Pure function, no DOM dependencies.
13
+ *
14
+ * Pipeline (order matters):
15
+ * 1. Strip incomplete tag at end
16
+ * 2. Strip complete <style>, <script>, and <head> blocks
17
+ * 3. Strip incomplete <style>/<script>/<head> blocks
18
+ * 4. Strip incomplete HTML entities
19
+ * 5. Extract body content (or use full string if no <body>)
20
+ */
21
+ export function processPartialHtml(html: string): string {
22
+ let result = html;
23
+
24
+ // 1. Strip incomplete tag at end — e.g. `<div class="fo`
25
+ result = result.replace(/<[^>]*$/, "");
26
+
27
+ // 2. Strip complete <style>, <script>, and <head> blocks
28
+ result = result.replace(/<(style|script|head)\b[^>]*>[\s\S]*?<\/\1>/gi, "");
29
+
30
+ // 3. Strip incomplete <style>/<script>/<head> blocks (opening tag, no close)
31
+ result = result.replace(/<(style|script|head)\b[^>]*>[\s\S]*$/gi, "");
32
+
33
+ // 4. Strip incomplete HTML entities — e.g. `&amp` without semicolon
34
+ result = result.replace(/&[a-zA-Z0-9#]*$/, "");
35
+
36
+ // 5. Extract body content
37
+ const bodyMatch = result.match(/<body[^>]*>([\s\S]*)/i);
38
+ if (bodyMatch) {
39
+ result = bodyMatch[1]!;
40
+ // Strip </body> and everything after
41
+ result = result.replace(/<\/body>[\s\S]*/i, "");
42
+ }
43
+
44
+ return result;
45
+ }