@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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
77
|
+
* {@link fetchMoreThreads}. Only meaningful when `limit` is set.
|
|
72
78
|
*/
|
|
73
|
-
|
|
79
|
+
hasMoreThreads: boolean;
|
|
74
80
|
/**
|
|
75
81
|
* `true` while a subsequent page of threads is being fetched.
|
|
76
82
|
*/
|
|
77
|
-
|
|
83
|
+
isFetchingMoreThreads: boolean;
|
|
78
84
|
/**
|
|
79
|
-
* Fetch the next page of threads. No-op when {@link
|
|
80
|
-
* `false` or a
|
|
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
|
-
|
|
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
|
|
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
|
|
176
|
-
const
|
|
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
|
|
263
|
+
const fetchMoreThreads = useCallback(() => store.fetchNextPage(), [store]);
|
|
244
264
|
|
|
245
265
|
return {
|
|
246
266
|
threads,
|
|
247
267
|
isLoading,
|
|
248
268
|
error,
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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 {
|
|
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 &")).toBe("<p>Hello ");
|
|
39
|
+
expect(processPartialHtml("<p>Hello {")).toBe("<p>Hello ");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("preserves complete entities", () => {
|
|
43
|
+
expect(processPartialHtml("<p>Hello & World</p>")).toBe(
|
|
44
|
+
"<p>Hello & 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. `&` 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
|
+
}
|