@copilotkit/react-core 1.55.0-next.9 → 1.55.1-next.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 +46 -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
@@ -9,12 +9,14 @@ import {
9
9
  type ReactNode,
10
10
  useMemo,
11
11
  useEffect,
12
+ useLayoutEffect,
12
13
  useReducer,
13
14
  useRef,
14
15
  useState,
15
16
  } from "react";
16
17
  import { z } from "zod";
17
18
  import { CopilotKitInspector } from "../components/CopilotKitInspector";
19
+ import type { Anchor } from "@copilotkit/web-inspector";
18
20
  import { LicenseWarningBanner } from "../components/license-warning-banner";
19
21
  import {
20
22
  createLicenseContextValue,
@@ -26,7 +28,16 @@ import {
26
28
  MCPAppsActivityRenderer,
27
29
  MCPAppsActivityType,
28
30
  } from "../components/MCPAppsActivityRenderer";
31
+ import {
32
+ OpenGenerativeUIActivityType,
33
+ OpenGenerativeUIContentSchema,
34
+ OpenGenerativeUIActivityRenderer,
35
+ OpenGenerativeUIToolRenderer,
36
+ GenerateSandboxedUiArgsSchema,
37
+ } from "../components/OpenGenerativeUIRenderer";
29
38
  import { createA2UIMessageRenderer } from "../a2ui/A2UIMessageRenderer";
39
+ import { A2UIBuiltInToolCallRenderer } from "../a2ui/A2UIToolCallRenderer";
40
+ import { A2UICatalogContext } from "../a2ui/A2UICatalogContext";
30
41
  import { viewerTheme } from "@copilotkit/a2ui-renderer";
31
42
  import type { Theme as A2UITheme } from "@copilotkit/a2ui-renderer";
32
43
  import { CopilotKitCoreReact } from "../lib/react-core";
@@ -37,10 +48,39 @@ import type {
37
48
  import type { ReactFrontendTool } from "../types/frontend-tool";
38
49
  import type { ReactHumanInTheLoop } from "../types/human-in-the-loop";
39
50
  import type { ReactCustomMessageRenderer } from "../types/react-custom-message-renderer";
51
+ import type { SandboxFunction } from "../types/sandbox-function";
52
+ import { SandboxFunctionsContext } from "./SandboxFunctionsContext";
53
+ import { schemaToJsonSchema } from "@copilotkit/shared";
54
+ import { zodToJsonSchema } from "zod-to-json-schema";
40
55
 
41
56
  const HEADER_NAME = "X-CopilotCloud-Public-Api-Key";
42
57
  const COPILOT_CLOUD_CHAT_URL = "https://api.cloud.copilotkit.ai/copilotkit/v1";
43
58
 
59
+ const DEFAULT_DESIGN_SKILL = `When generating UI with generateSandboxedUi, follow these design principles inspired by shadcn/ui:
60
+
61
+ - Use a minimal, flat aesthetic. Avoid drop shadows and gradients — rely on subtle borders (1px solid, light gray like #e5e7eb) to define surfaces.
62
+ - Neutral base palette: white backgrounds, zinc/slate gray text (#09090b for headings, #71717a for secondary text). One accent color for interactive elements.
63
+ - Use system font stacks (system-ui, -apple-system, sans-serif) at readable sizes (14px body, 600 weight for headings). Tight line-heights.
64
+ - Small, consistent border-radius (6–8px). Cards and containers use border, not shadow, for separation.
65
+ - Buttons: solid fill for primary (dark bg, white text), outline for secondary (border + transparent bg). Subtle hover state (slight opacity or background shift).
66
+ - Use CSS Grid or Flexbox for layout. Ensure the UI looks good at any width.
67
+ - Minimal transitions (150ms) for hover/focus states only. No decorative animations.
68
+ - Keep the UI focused and dense — avoid excessive padding. Use compact spacing (8–12px gaps, 10–14px padding in controls).`;
69
+
70
+ const GENERATE_SANDBOXED_UI_DESCRIPTION =
71
+ "Generate sandboxed UI. " +
72
+ "IMPORTANT: The generated code runs in a sandboxed iframe WITHOUT same-origin access. " +
73
+ "Do NOT use localStorage, sessionStorage, document.cookie, IndexedDB, or fetch/XMLHttpRequest to same-origin URLs. " +
74
+ "To communicate with the host application, use Websandbox.connection.remote.<functionName>(args) which returns a Promise.\n\n" +
75
+ "You CAN use external libraries from CDNs by including <script> or <link> tags in the HTML <head> (e.g., Chart.js, D3, Three.js, x-data-spreadsheet, etc.). " +
76
+ "CDN resources load normally inside the sandbox.\n\n" +
77
+ "PARAMETER ORDER IS CRITICAL — generate parameters in exactly this order:\n" +
78
+ "1. initialHeight + placeholderMessages (shown to user while generating)\n" +
79
+ "2. css (all styles FIRST — the user sees a placeholder until CSS is complete)\n" +
80
+ "3. html (streams in live — the user watches the UI build as HTML is generated)\n" +
81
+ "4. jsFunctions (reusable helper functions)\n" +
82
+ "5. jsExpressions (applied one-by-one — the user sees each expression take effect)";
83
+
44
84
  // Define the context value interface - idiomatic React naming
45
85
  export interface CopilotKitContextValue {
46
86
  copilotkit: CopilotKitCoreReact;
@@ -99,6 +139,39 @@ export interface CopilotKitProviderProps {
99
139
  renderCustomMessages?: ReactCustomMessageRenderer[];
100
140
  frontendTools?: ReactFrontendTool[];
101
141
  humanInTheLoop?: ReactHumanInTheLoop[];
142
+ /**
143
+ * Configuration for OpenGenerativeUI — sandboxed UI generated by the LLM.
144
+ *
145
+ * @example
146
+ * ```tsx
147
+ * <CopilotKit
148
+ * runtimeUrl="/api/copilotkit"
149
+ * openGenerativeUI={{
150
+ * sandboxFunctions: [{ name: "addToCart", description: "…", parameters: schema, handler: fn }],
151
+ * }}
152
+ * >
153
+ * ```
154
+ */
155
+ openGenerativeUI?: {
156
+ /**
157
+ * Functions made available inside sandboxed iframes.
158
+ * Each function is described to the LLM via agent context and exposed
159
+ * via websandbox's `localApi`.
160
+ *
161
+ * Inside the iframe, call them with:
162
+ * ```js
163
+ * await Websandbox.connection.remote.<functionName>(args)
164
+ * ```
165
+ */
166
+ sandboxFunctions?: SandboxFunction[];
167
+ /**
168
+ * Design guidelines injected as agent context for the `generateSandboxedUi` tool.
169
+ * Override this to control the visual style of generated UIs.
170
+ *
171
+ * A sensible default is provided if omitted.
172
+ */
173
+ designSkill?: string;
174
+ };
102
175
  showDevConsole?: boolean | "auto";
103
176
  /**
104
177
  * Error handler called when CopilotKit encounters an error.
@@ -128,7 +201,39 @@ export interface CopilotKitProviderProps {
128
201
  * When omitted, the built-in `viewerTheme` from `@copilotkit/a2ui-renderer` is used.
129
202
  */
130
203
  theme?: A2UITheme;
204
+ /**
205
+ * Optional component catalog to pass to the A2UI renderer.
206
+ * When omitted, the default basicCatalog is used.
207
+ */
208
+ catalog?: any;
209
+ /**
210
+ * Optional custom loading component shown while an A2UI surface is generating.
211
+ * When omitted, a default animated skeleton is shown.
212
+ */
213
+ loadingComponent?: React.ComponentType;
214
+ /**
215
+ * When true (the default), the full component schemas from the catalog are
216
+ * sent as agent context so the agent knows what components and props are
217
+ * available. The A2UI middleware can overwrite this with a server-side
218
+ * schema if configured. Set to false to disable.
219
+ */
220
+ includeSchema?: boolean;
131
221
  };
222
+ /**
223
+ * Default throttle interval (in milliseconds) for `useAgent` re-renders
224
+ * triggered by `OnMessagesChanged` notifications. This value is used as
225
+ * a fallback when neither the `useAgent()` hook nor `<CopilotChat>` /
226
+ * `<CopilotSidebar>` / `<CopilotPopup>` specify an explicit `throttleMs`.
227
+ *
228
+ * @default undefined (components/hooks without an explicit throttleMs will be unthrottled)
229
+ */
230
+ defaultThrottleMs?: number;
231
+ /**
232
+ * Default anchor corner for the inspector button and window.
233
+ * Only used on first load before the user drags to a custom position.
234
+ * Defaults to `{ horizontal: "right", vertical: "top" }`.
235
+ */
236
+ inspectorDefaultAnchor?: Anchor;
132
237
  }
133
238
 
134
239
  // Small helper to normalize array props to a stable reference and warn
@@ -171,13 +276,18 @@ export const CopilotKitProvider: React.FC<CopilotKitProviderProps> = ({
171
276
  renderCustomMessages,
172
277
  frontendTools,
173
278
  humanInTheLoop,
279
+ openGenerativeUI,
174
280
  showDevConsole = false,
175
- useSingleEndpoint = false,
281
+ useSingleEndpoint,
176
282
  onError,
177
283
  a2ui,
284
+ defaultThrottleMs,
285
+ inspectorDefaultAnchor,
178
286
  }) => {
179
287
  const [shouldRenderInspector, setShouldRenderInspector] = useState(false);
180
288
  const [runtimeA2UIEnabled, setRuntimeA2UIEnabled] = useState(false);
289
+ const [runtimeOpenGenUIEnabled, setRuntimeOpenGenUIEnabled] = useState(false);
290
+ const openGenUIActive = runtimeOpenGenUIEnabled || !!openGenerativeUI;
181
291
  const [runtimeLicenseStatus, setRuntimeLicenseStatus] = useState<
182
292
  string | undefined
183
293
  >(undefined);
@@ -245,14 +355,26 @@ export const CopilotKitProvider: React.FC<CopilotKitProviderProps> = ({
245
355
  },
246
356
  ];
247
357
 
358
+ if (openGenUIActive) {
359
+ renderers.push({
360
+ activityType: OpenGenerativeUIActivityType,
361
+ content: OpenGenerativeUIContentSchema,
362
+ render: OpenGenerativeUIActivityRenderer,
363
+ });
364
+ }
365
+
248
366
  if (runtimeA2UIEnabled) {
249
367
  renderers.unshift(
250
- createA2UIMessageRenderer({ theme: a2ui?.theme ?? viewerTheme }),
368
+ createA2UIMessageRenderer({
369
+ theme: a2ui?.theme ?? viewerTheme,
370
+ catalog: a2ui?.catalog,
371
+ loadingComponent: a2ui?.loadingComponent,
372
+ }),
251
373
  );
252
374
  }
253
375
 
254
376
  return renderers;
255
- }, [runtimeA2UIEnabled, a2ui]);
377
+ }, [runtimeA2UIEnabled, openGenUIActive, a2ui]);
256
378
 
257
379
  // Combine user-provided activity renderers with built-in ones
258
380
  // User-provided renderers take precedence (come first) so they can override built-ins
@@ -299,6 +421,10 @@ export const CopilotKitProvider: React.FC<CopilotKitProviderProps> = ({
299
421
  humanInTheLoop,
300
422
  "humanInTheLoop must be a stable array. If you want to dynamically add or remove human-in-the-loop tools, use `useHumanInTheLoop` instead.",
301
423
  );
424
+ const sandboxFunctionsList = useStableArrayProp<SandboxFunction>(
425
+ openGenerativeUI?.sandboxFunctions,
426
+ "openGenerativeUI.sandboxFunctions must be a stable array.",
427
+ );
302
428
 
303
429
  // Note: warnings for array identity changes are handled by useStableArrayProp
304
430
 
@@ -344,25 +470,41 @@ export const CopilotKitProvider: React.FC<CopilotKitProviderProps> = ({
344
470
  return { tools: processedTools, renderToolCalls: processedRenderToolCalls };
345
471
  }, [humanInTheLoopList]);
346
472
 
473
+ // Built-in frontend tool for generateSandboxedUi — registered only when the runtime has openGenerativeUI enabled
474
+ const builtInFrontendTools = useMemo<ReactFrontendTool[]>(() => {
475
+ if (!openGenUIActive) return [];
476
+ return [
477
+ {
478
+ name: "generateSandboxedUi",
479
+ description: GENERATE_SANDBOXED_UI_DESCRIPTION,
480
+ parameters: GenerateSandboxedUiArgsSchema,
481
+ handler: async () => "UI generated",
482
+ followUp: true,
483
+ render: OpenGenerativeUIToolRenderer,
484
+ },
485
+ ];
486
+ }, [openGenUIActive]);
487
+
347
488
  // Combine all tools for CopilotKitCore
348
489
  const allTools = useMemo(() => {
349
490
  const tools: FrontendTool[] = [];
350
491
 
351
- // Add frontend tools
492
+ // Add frontend tools (user-provided + built-in)
352
493
  tools.push(...frontendToolsList);
494
+ tools.push(...builtInFrontendTools);
353
495
 
354
496
  // Add processed human-in-the-loop tools
355
497
  tools.push(...processedHumanInTheLoopTools.tools);
356
498
 
357
499
  return tools;
358
- }, [frontendToolsList, processedHumanInTheLoopTools]);
500
+ }, [frontendToolsList, builtInFrontendTools, processedHumanInTheLoopTools]);
359
501
 
360
502
  // Combine all render tool calls
361
503
  const allRenderToolCalls = useMemo(() => {
362
504
  const combined: ReactToolCallRenderer<unknown>[] = [...renderToolCallsList];
363
505
 
364
- // Add render components from frontend tools
365
- frontendToolsList.forEach((tool) => {
506
+ // Add render components from frontend tools (user-provided + built-in)
507
+ [...frontendToolsList, ...builtInFrontendTools].forEach((tool) => {
366
508
  if (tool.render) {
367
509
  // For wildcard tools without parameters, default to z.any()
368
510
  const args =
@@ -381,7 +523,12 @@ export const CopilotKitProvider: React.FC<CopilotKitProviderProps> = ({
381
523
  combined.push(...processedHumanInTheLoopTools.renderToolCalls);
382
524
 
383
525
  return combined;
384
- }, [renderToolCallsList, frontendToolsList, processedHumanInTheLoopTools]);
526
+ }, [
527
+ renderToolCallsList,
528
+ frontendToolsList,
529
+ builtInFrontendTools,
530
+ processedHumanInTheLoopTools,
531
+ ]);
385
532
 
386
533
  // Stable instance: created once for the provider lifetime.
387
534
  // Updates are applied via setter effects below rather than recreating the instance.
@@ -389,7 +536,12 @@ export const CopilotKitProvider: React.FC<CopilotKitProviderProps> = ({
389
536
  if (copilotkitRef.current === null) {
390
537
  copilotkitRef.current = new CopilotKitCoreReact({
391
538
  runtimeUrl: chatApiEndpoint,
392
- runtimeTransport: useSingleEndpoint ? "single" : "rest",
539
+ runtimeTransport:
540
+ useSingleEndpoint === true
541
+ ? "single"
542
+ : useSingleEndpoint === false
543
+ ? "rest"
544
+ : "auto",
393
545
  headers: mergedHeaders,
394
546
  credentials,
395
547
  properties,
@@ -399,14 +551,22 @@ export const CopilotKitProvider: React.FC<CopilotKitProviderProps> = ({
399
551
  renderActivityMessages: allActivityRenderers,
400
552
  renderCustomMessages: renderCustomMessagesList,
401
553
  });
554
+ // Set initial defaultThrottleMs synchronously so child hooks see the
555
+ // correct value on their first render (before useEffect fires).
556
+ if (defaultThrottleMs !== undefined) {
557
+ copilotkitRef.current.setDefaultThrottleMs(defaultThrottleMs);
558
+ }
402
559
  }
403
560
  const copilotkit = copilotkitRef.current;
404
561
 
405
- // Sync runtime-reported state once runtime info is fetched
562
+ // Sync runtime feature flags from the core once runtime info is fetched
406
563
  useEffect(() => {
564
+ // Check current value immediately (may already be set before subscription)
565
+ setRuntimeA2UIEnabled(copilotkit.a2uiEnabled);
407
566
  const subscription = copilotkit.subscribe({
408
567
  onRuntimeConnectionStatusChanged: () => {
409
568
  setRuntimeA2UIEnabled(copilotkit.a2uiEnabled);
569
+ setRuntimeOpenGenUIEnabled(copilotkit.openGenerativeUIEnabled);
410
570
  setRuntimeLicenseStatus(copilotkit.licenseStatus);
411
571
  },
412
572
  });
@@ -490,7 +650,13 @@ export const CopilotKitProvider: React.FC<CopilotKitProviderProps> = ({
490
650
 
491
651
  useEffect(() => {
492
652
  copilotkit.setRuntimeUrl(chatApiEndpoint);
493
- copilotkit.setRuntimeTransport(useSingleEndpoint ? "single" : "rest");
653
+ copilotkit.setRuntimeTransport(
654
+ useSingleEndpoint === true
655
+ ? "single"
656
+ : useSingleEndpoint === false
657
+ ? "rest"
658
+ : "auto",
659
+ );
494
660
  copilotkit.setHeaders(mergedHeaders);
495
661
  copilotkit.setCredentials(credentials);
496
662
  copilotkit.setProperties(properties);
@@ -540,6 +706,64 @@ export const CopilotKitProvider: React.FC<CopilotKitProviderProps> = ({
540
706
  didMountRef.current = true;
541
707
  }, []);
542
708
 
709
+ // Sync defaultThrottleMs to the core instance on prop changes.
710
+ // Initial value is set synchronously during instance creation (below the
711
+ // ref init) so child hooks see the correct value on their first render.
712
+ // This effect handles subsequent updates when the prop changes.
713
+ useEffect(() => {
714
+ if (
715
+ defaultThrottleMs !== undefined &&
716
+ (!Number.isFinite(defaultThrottleMs) || defaultThrottleMs < 0)
717
+ ) {
718
+ console.error(
719
+ `CopilotKitProvider: defaultThrottleMs must be a non-negative finite number, got ${defaultThrottleMs}. ` +
720
+ `useAgent hooks without an explicit throttleMs will fall back to unthrottled.`,
721
+ );
722
+ }
723
+ copilotkit.setDefaultThrottleMs(defaultThrottleMs);
724
+ }, [copilotkit, defaultThrottleMs]);
725
+
726
+ // Register design skill as agent context for the generateSandboxedUi tool
727
+ const designSkill = openGenerativeUI?.designSkill ?? DEFAULT_DESIGN_SKILL;
728
+
729
+ useLayoutEffect(() => {
730
+ if (!copilotkit || !openGenUIActive) return;
731
+
732
+ const id = copilotkit.addContext({
733
+ description:
734
+ "Design guidelines for the generateSandboxedUi tool. Follow these when building UI.",
735
+ value: designSkill,
736
+ });
737
+ return () => {
738
+ copilotkit.removeContext(id);
739
+ };
740
+ }, [copilotkit, designSkill, openGenUIActive]);
741
+
742
+ // Register sandbox functions as agent context so the LLM knows how to call them
743
+ const sandboxFunctionsDescriptors = useMemo(() => {
744
+ if (sandboxFunctionsList.length === 0) return null;
745
+ return JSON.stringify(
746
+ sandboxFunctionsList.map((fn) => ({
747
+ name: fn.name,
748
+ description: fn.description,
749
+ parameters: schemaToJsonSchema(fn.parameters, { zodToJsonSchema }),
750
+ })),
751
+ );
752
+ }, [sandboxFunctionsList]);
753
+
754
+ useLayoutEffect(() => {
755
+ if (!copilotkit || !sandboxFunctionsDescriptors || !openGenUIActive) return;
756
+
757
+ const id = copilotkit.addContext({
758
+ description:
759
+ "Sandbox functions available in generated sandboxed UI code. Call via: await Websandbox.connection.remote.<functionName>(args)",
760
+ value: sandboxFunctionsDescriptors,
761
+ });
762
+ return () => {
763
+ copilotkit.removeContext(id);
764
+ };
765
+ }, [copilotkit, sandboxFunctionsDescriptors, openGenUIActive]);
766
+
543
767
  const contextValue = useMemo<CopilotKitContextValue>(
544
768
  () => ({ copilotkit, executingToolCallIds }),
545
769
  [copilotkit, executingToolCallIds],
@@ -552,27 +776,39 @@ export const CopilotKitProvider: React.FC<CopilotKitProviderProps> = ({
552
776
  );
553
777
 
554
778
  return (
555
- <CopilotKitContext.Provider value={contextValue}>
556
- <LicenseContext.Provider value={licenseContextValue}>
557
- {children}
558
- {shouldRenderInspector ? (
559
- <CopilotKitInspector core={copilotkit} />
560
- ) : null}
561
- {/* License warnings — driven by server-reported status */}
562
- {runtimeLicenseStatus === "none" && !resolvedPublicKey && (
563
- <LicenseWarningBanner type="no_license" />
564
- )}
565
- {runtimeLicenseStatus === "expired" && (
566
- <LicenseWarningBanner type="expired" />
567
- )}
568
- {runtimeLicenseStatus === "invalid" && (
569
- <LicenseWarningBanner type="invalid" />
570
- )}
571
- {runtimeLicenseStatus === "expiring" && (
572
- <LicenseWarningBanner type="expiring" />
573
- )}
574
- </LicenseContext.Provider>
575
- </CopilotKitContext.Provider>
779
+ <SandboxFunctionsContext.Provider value={sandboxFunctionsList}>
780
+ <CopilotKitContext.Provider value={contextValue}>
781
+ <LicenseContext.Provider value={licenseContextValue}>
782
+ {runtimeA2UIEnabled && <A2UIBuiltInToolCallRenderer />}
783
+ {runtimeA2UIEnabled && (
784
+ <A2UICatalogContext
785
+ catalog={a2ui?.catalog}
786
+ includeSchema={a2ui?.includeSchema}
787
+ />
788
+ )}
789
+ {children}
790
+ {shouldRenderInspector ? (
791
+ <CopilotKitInspector
792
+ core={copilotkit}
793
+ defaultAnchor={inspectorDefaultAnchor}
794
+ />
795
+ ) : null}
796
+ {/* License warnings — driven by server-reported status */}
797
+ {runtimeLicenseStatus === "none" && !resolvedPublicKey && (
798
+ <LicenseWarningBanner type="no_license" />
799
+ )}
800
+ {runtimeLicenseStatus === "expired" && (
801
+ <LicenseWarningBanner type="expired" />
802
+ )}
803
+ {runtimeLicenseStatus === "invalid" && (
804
+ <LicenseWarningBanner type="invalid" />
805
+ )}
806
+ {runtimeLicenseStatus === "expiring" && (
807
+ <LicenseWarningBanner type="expiring" />
808
+ )}
809
+ </LicenseContext.Provider>
810
+ </CopilotKitContext.Provider>
811
+ </SandboxFunctionsContext.Provider>
576
812
  );
577
813
  };
578
814
 
@@ -0,0 +1,10 @@
1
+ import { createContext, useContext } from "react";
2
+ import type { SandboxFunction } from "../types/sandbox-function";
3
+
4
+ export const SandboxFunctionsContext = createContext<
5
+ readonly SandboxFunction[]
6
+ >([]);
7
+
8
+ export function useSandboxFunctions(): readonly SandboxFunction[] {
9
+ return useContext(SandboxFunctionsContext);
10
+ }
@@ -0,0 +1,198 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import React from "react";
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { z } from "zod";
5
+ import { CopilotKitProvider, useCopilotKit } from "../CopilotKitProvider";
6
+ import { useSandboxFunctions } from "../SandboxFunctionsContext";
7
+ import type { SandboxFunction } from "../../types/sandbox-function";
8
+
9
+ describe("CopilotKitProvider — openGenerativeUI.sandboxFunctions", () => {
10
+ beforeEach(() => {
11
+ vi.spyOn(console, "error").mockImplementation(() => {});
12
+ vi.spyOn(console, "warn").mockImplementation(() => {});
13
+ });
14
+
15
+ afterEach(() => {
16
+ vi.restoreAllMocks();
17
+ });
18
+
19
+ const makeSandboxFunction = (
20
+ name: string,
21
+ overrides?: Partial<SandboxFunction>,
22
+ ): SandboxFunction => ({
23
+ name,
24
+ description: `${name} description`,
25
+ parameters: z.object({ value: z.string() }),
26
+ handler: vi.fn().mockResolvedValue(undefined),
27
+ ...overrides,
28
+ });
29
+
30
+ /** Helper: find the sandbox-functions context entry from the context record */
31
+ function findSandboxContext(
32
+ ctx: Record<string, { description: string; value: string }>,
33
+ ) {
34
+ return Object.values(ctx).find((c) =>
35
+ c.description.includes("Sandbox functions"),
36
+ );
37
+ }
38
+
39
+ describe("SandboxFunctionsContext", () => {
40
+ it("provides sandbox functions to children via context", () => {
41
+ const fns = [makeSandboxFunction("myFn")];
42
+
43
+ const { result } = renderHook(() => useSandboxFunctions(), {
44
+ wrapper: ({ children }) => (
45
+ <CopilotKitProvider openGenerativeUI={{ sandboxFunctions: fns }}>
46
+ {children}
47
+ </CopilotKitProvider>
48
+ ),
49
+ });
50
+
51
+ expect(result.current).toHaveLength(1);
52
+ expect(result.current[0].name).toBe("myFn");
53
+ });
54
+
55
+ it("provides empty array when openGenerativeUI is not set", () => {
56
+ const { result } = renderHook(() => useSandboxFunctions(), {
57
+ wrapper: ({ children }) => (
58
+ <CopilotKitProvider>{children}</CopilotKitProvider>
59
+ ),
60
+ });
61
+
62
+ expect(result.current).toHaveLength(0);
63
+ });
64
+
65
+ it("provides empty array when sandboxFunctions is not set", () => {
66
+ const { result } = renderHook(() => useSandboxFunctions(), {
67
+ wrapper: ({ children }) => (
68
+ <CopilotKitProvider openGenerativeUI={{}}>
69
+ {children}
70
+ </CopilotKitProvider>
71
+ ),
72
+ });
73
+
74
+ expect(result.current).toHaveLength(0);
75
+ });
76
+ });
77
+
78
+ describe("agent context registration", () => {
79
+ it("registers agent context when sandbox functions are provided", () => {
80
+ const fns = [makeSandboxFunction("addToCart")];
81
+
82
+ const { result } = renderHook(() => useCopilotKit(), {
83
+ wrapper: ({ children }) => (
84
+ <CopilotKitProvider openGenerativeUI={{ sandboxFunctions: fns }}>
85
+ {children}
86
+ </CopilotKitProvider>
87
+ ),
88
+ });
89
+
90
+ const sandboxContext = findSandboxContext(
91
+ result.current.copilotkit.context,
92
+ );
93
+ expect(sandboxContext).toBeDefined();
94
+
95
+ const parsed = JSON.parse(sandboxContext!.value);
96
+ expect(parsed).toHaveLength(1);
97
+ expect(parsed[0].name).toBe("addToCart");
98
+ expect(parsed[0].description).toBe("addToCart description");
99
+ expect(parsed[0].parameters).toBeDefined();
100
+ expect(parsed[0].parameters.type).toBe("object");
101
+ });
102
+
103
+ it("does not register agent context when sandbox functions are empty", () => {
104
+ const { result } = renderHook(() => useCopilotKit(), {
105
+ wrapper: ({ children }) => (
106
+ <CopilotKitProvider openGenerativeUI={{ sandboxFunctions: [] }}>
107
+ {children}
108
+ </CopilotKitProvider>
109
+ ),
110
+ });
111
+
112
+ const sandboxContext = findSandboxContext(
113
+ result.current.copilotkit.context,
114
+ );
115
+ expect(sandboxContext).toBeUndefined();
116
+ });
117
+
118
+ it("does not register agent context when openGenerativeUI is omitted", () => {
119
+ const { result } = renderHook(() => useCopilotKit(), {
120
+ wrapper: ({ children }) => (
121
+ <CopilotKitProvider>{children}</CopilotKitProvider>
122
+ ),
123
+ });
124
+
125
+ const sandboxContext = findSandboxContext(
126
+ result.current.copilotkit.context,
127
+ );
128
+ expect(sandboxContext).toBeUndefined();
129
+ });
130
+
131
+ it("includes multiple functions in agent context", () => {
132
+ const fns = [makeSandboxFunction("fnA"), makeSandboxFunction("fnB")];
133
+
134
+ const { result } = renderHook(() => useCopilotKit(), {
135
+ wrapper: ({ children }) => (
136
+ <CopilotKitProvider openGenerativeUI={{ sandboxFunctions: fns }}>
137
+ {children}
138
+ </CopilotKitProvider>
139
+ ),
140
+ });
141
+
142
+ const sandboxContext = findSandboxContext(
143
+ result.current.copilotkit.context,
144
+ );
145
+ const parsed = JSON.parse(sandboxContext!.value);
146
+ expect(parsed).toHaveLength(2);
147
+ expect(parsed.map((f: any) => f.name)).toEqual(["fnA", "fnB"]);
148
+ });
149
+
150
+ it("converts parameters to JSON Schema in agent context", () => {
151
+ const fns = [
152
+ makeSandboxFunction("myFn", {
153
+ parameters: z.object({
154
+ itemId: z.string(),
155
+ quantity: z.number(),
156
+ }),
157
+ }),
158
+ ];
159
+
160
+ const { result } = renderHook(() => useCopilotKit(), {
161
+ wrapper: ({ children }) => (
162
+ <CopilotKitProvider openGenerativeUI={{ sandboxFunctions: fns }}>
163
+ {children}
164
+ </CopilotKitProvider>
165
+ ),
166
+ });
167
+
168
+ const sandboxContext = findSandboxContext(
169
+ result.current.copilotkit.context,
170
+ );
171
+ const parsed = JSON.parse(sandboxContext!.value);
172
+ const params = parsed[0].parameters;
173
+
174
+ expect(params.type).toBe("object");
175
+ expect(params.properties.itemId).toEqual({ type: "string" });
176
+ expect(params.properties.quantity).toEqual({ type: "number" });
177
+ });
178
+
179
+ it("removes agent context on unmount", () => {
180
+ const fns = [makeSandboxFunction("myFn")];
181
+
182
+ const { result, unmount } = renderHook(() => useCopilotKit(), {
183
+ wrapper: ({ children }) => (
184
+ <CopilotKitProvider openGenerativeUI={{ sandboxFunctions: fns }}>
185
+ {children}
186
+ </CopilotKitProvider>
187
+ ),
188
+ });
189
+
190
+ const copilotkit = result.current.copilotkit;
191
+ expect(findSandboxContext(copilotkit.context)).toBeDefined();
192
+
193
+ unmount();
194
+
195
+ expect(findSandboxContext(copilotkit.context)).toBeUndefined();
196
+ });
197
+ });
198
+ });