@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
@@ -22,19 +22,25 @@ describe("A2UIMessageRenderer rendering integration", () => {
22
22
  });
23
23
 
24
24
  const content = {
25
- operations: [
25
+ a2ui_operations: [
26
26
  {
27
- beginRendering: {
27
+ version: "v0.9",
28
+ createSurface: {
28
29
  surfaceId: "test-surface",
29
- root: "root",
30
- styles: {},
30
+ catalogId: "https://a2ui.org/specification/v0_9/basic_catalog.json",
31
31
  },
32
32
  },
33
33
  {
34
- surfaceUpdate: {
34
+ version: "v0.9",
35
+ updateComponents: {
35
36
  surfaceId: "test-surface",
36
37
  components: [
37
- { id: "root", text: { literalString: "Hello World" } },
38
+ {
39
+ id: "root",
40
+ component: "Text",
41
+ text: "Hello World",
42
+ variant: "body",
43
+ },
38
44
  ],
39
45
  },
40
46
  },
@@ -69,12 +75,27 @@ describe("A2UIMessageRenderer rendering integration", () => {
69
75
  let setContent: (content: any) => void;
70
76
  const TestWrapper = () => {
71
77
  const [content, _setContent] = useState({
72
- operations: [
73
- { beginRendering: { surfaceId: "test", root: "root", styles: {} } },
78
+ a2ui_operations: [
74
79
  {
75
- surfaceUpdate: {
80
+ version: "v0.9",
81
+ createSurface: {
76
82
  surfaceId: "test",
77
- components: [{ id: "root", text: { literalString: "Initial" } }],
83
+ catalogId:
84
+ "https://a2ui.org/specification/v0_9/basic_catalog.json",
85
+ },
86
+ },
87
+ {
88
+ version: "v0.9",
89
+ updateComponents: {
90
+ surfaceId: "test",
91
+ components: [
92
+ {
93
+ id: "root",
94
+ component: "Text",
95
+ text: "Initial",
96
+ variant: "body",
97
+ },
98
+ ],
78
99
  },
79
100
  },
80
101
  ],
@@ -94,12 +115,27 @@ describe("A2UIMessageRenderer rendering integration", () => {
94
115
 
95
116
  await act(async () => {
96
117
  setContent({
97
- operations: [
98
- { beginRendering: { surfaceId: "test", root: "root", styles: {} } },
118
+ a2ui_operations: [
119
+ {
120
+ version: "v0.9",
121
+ createSurface: {
122
+ surfaceId: "test",
123
+ catalogId:
124
+ "https://a2ui.org/specification/v0_9/basic_catalog.json",
125
+ },
126
+ },
99
127
  {
100
- surfaceUpdate: {
128
+ version: "v0.9",
129
+ updateComponents: {
101
130
  surfaceId: "test",
102
- components: [{ id: "root", text: { literalString: "Updated" } }],
131
+ components: [
132
+ {
133
+ id: "root",
134
+ component: "Text",
135
+ text: "Updated",
136
+ variant: "body",
137
+ },
138
+ ],
103
139
  },
104
140
  },
105
141
  ],
@@ -119,7 +155,7 @@ describe("A2UIMessageRenderer rendering integration", () => {
119
155
  const RenderComponent = renderer.render as React.FC<any>;
120
156
 
121
157
  const TestWrapper = () => (
122
- <RenderComponent content={{ operations: [] }} agent={null} />
158
+ <RenderComponent content={{ a2ui_operations: [] }} agent={null} />
123
159
  );
124
160
 
125
161
  let container: HTMLElement;
@@ -140,19 +176,47 @@ describe("A2UIMessageRenderer rendering integration", () => {
140
176
  const RenderComponent = renderer.render as React.FC<any>;
141
177
 
142
178
  const content = {
143
- operations: [
144
- { beginRendering: { surfaceId: "s1", root: "r1", styles: {} } },
145
- { beginRendering: { surfaceId: "s2", root: "r2", styles: {} } },
179
+ a2ui_operations: [
180
+ {
181
+ version: "v0.9",
182
+ createSurface: {
183
+ surfaceId: "s1",
184
+ catalogId: "https://a2ui.org/specification/v0_9/basic_catalog.json",
185
+ },
186
+ },
187
+ {
188
+ version: "v0.9",
189
+ createSurface: {
190
+ surfaceId: "s2",
191
+ catalogId: "https://a2ui.org/specification/v0_9/basic_catalog.json",
192
+ },
193
+ },
146
194
  {
147
- surfaceUpdate: {
195
+ version: "v0.9",
196
+ updateComponents: {
148
197
  surfaceId: "s1",
149
- components: [{ id: "r1", text: { literalString: "Surface 1" } }],
198
+ components: [
199
+ {
200
+ id: "root",
201
+ component: "Text",
202
+ text: "Surface 1",
203
+ variant: "body",
204
+ },
205
+ ],
150
206
  },
151
207
  },
152
208
  {
153
- surfaceUpdate: {
209
+ version: "v0.9",
210
+ updateComponents: {
154
211
  surfaceId: "s2",
155
- components: [{ id: "r2", text: { literalString: "Surface 2" } }],
212
+ components: [
213
+ {
214
+ id: "root",
215
+ component: "Text",
216
+ text: "Surface 2",
217
+ variant: "body",
218
+ },
219
+ ],
156
220
  },
157
221
  },
158
222
  ],
@@ -51,9 +51,18 @@ export class MockStepwiseAgent extends AbstractAgent {
51
51
  });
52
52
  }
53
53
 
54
- clone(): MockStepwiseAgent {
55
- // For tests, return same instance so we can keep controlling it
56
- return this;
54
+ clone(): this {
55
+ // Return a new instance that shares the same subject so tests can keep
56
+ // controlling events via the original reference while satisfying the
57
+ // clone() contract (must return a distinct object).
58
+ // Use the concrete constructor so subclasses (e.g. FailingConnectAgent)
59
+ // preserve their overridden methods.
60
+ const cloned = new (this
61
+ .constructor as new () => MockStepwiseAgent)() as this;
62
+ cloned.agentId = this.agentId;
63
+ (cloned as unknown as { subject: Subject<BaseEvent> }).subject =
64
+ this.subject;
65
+ return cloned;
57
66
  }
58
67
 
59
68
  // No-op: prevent the base class from tearing down the Subject
@@ -110,7 +119,21 @@ export class MockReconnectableAgent extends AbstractAgent {
110
119
  }
111
120
 
112
121
  clone(): MockReconnectableAgent {
113
- return this;
122
+ const cloned = new MockReconnectableAgent();
123
+ cloned.agentId = this.agentId;
124
+ (
125
+ cloned as unknown as {
126
+ subject: Subject<BaseEvent>;
127
+ storedEvents: BaseEvent[];
128
+ }
129
+ ).subject = this.subject;
130
+ (
131
+ cloned as unknown as {
132
+ subject: Subject<BaseEvent>;
133
+ storedEvents: BaseEvent[];
134
+ }
135
+ ).storedEvents = this.storedEvents;
136
+ return cloned;
114
137
  }
115
138
 
116
139
  // No-op: prevent the base class from tearing down the Subject
@@ -141,6 +164,7 @@ export function renderWithCopilotKit({
141
164
  humanInTheLoop,
142
165
  agentId,
143
166
  threadId,
167
+ defaultThrottleMs,
144
168
  children,
145
169
  }: {
146
170
  agent?: AbstractAgent;
@@ -152,6 +176,7 @@ export function renderWithCopilotKit({
152
176
  humanInTheLoop?: any[];
153
177
  agentId?: string;
154
178
  threadId?: string;
179
+ defaultThrottleMs?: number;
155
180
  children?: React.ReactNode;
156
181
  }): ReturnType<typeof render> {
157
182
  const resolvedAgents = agents || (agent ? { default: agent } : undefined);
@@ -166,6 +191,7 @@ export function renderWithCopilotKit({
166
191
  renderActivityMessages={renderActivityMessages}
167
192
  frontendTools={frontendTools}
168
193
  humanInTheLoop={humanInTheLoop}
194
+ defaultThrottleMs={defaultThrottleMs}
169
195
  >
170
196
  <CopilotChatConfigurationProvider
171
197
  agentId={resolvedAgentId}
@@ -394,6 +420,70 @@ export function testId(prefix: string): string {
394
420
  return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
395
421
  }
396
422
 
423
+ // Varied content lengths for realistic message sizes in perf tests.
424
+ const SAMPLE_ASSISTANT_TEXTS = [
425
+ "Sure! I'd be happy to help you with that.",
426
+ "The weather in San Francisco today is 65°F with partly cloudy skies.",
427
+ "Here are the main points from the meeting: 1) Roadmap review, 2) Bug triage, 3) Release planning.",
428
+ "To configure a custom agent, extend AbstractAgent and implement the run() method. Register it with CopilotKitProvider via the agents__unsafe_dev_only prop.",
429
+ "Here is a React component that fetches data from an API endpoint using useEffect and useState.",
430
+ ];
431
+
432
+ /**
433
+ * Generate a realistic sequence of BaseEvents for N assistant messages.
434
+ * Uses TEXT_MESSAGE_CHUNK (the only event type proven to create rendered messages
435
+ * in jsdom tests). Every 5th message includes a tool call.
436
+ *
437
+ * Wrap in RUN_STARTED / RUN_FINISHED yourself if you need a full run sequence:
438
+ * @example
439
+ * agent.emit(runStartedEvent());
440
+ * for (const event of generateMessages(100)) agent.emit(event);
441
+ * agent.emit(runFinishedEvent());
442
+ */
443
+ export function generateMessages(n: number): BaseEvent[] {
444
+ const events: BaseEvent[] = [];
445
+
446
+ for (let i = 0; i < n; i++) {
447
+ const msgId = `gen-msg-${i}`;
448
+ const text = SAMPLE_ASSISTANT_TEXTS[i % SAMPLE_ASSISTANT_TEXTS.length];
449
+
450
+ // Stream content in ~20-char chunks to simulate real streaming
451
+ for (let offset = 0; offset < text.length; offset += 20) {
452
+ events.push(textChunkEvent(msgId, text.slice(offset, offset + 20)));
453
+ }
454
+
455
+ // Every 5th message has a tool call for realistic variety
456
+ if (i % 5 === 4) {
457
+ const tcId = `gen-tc-${i}`;
458
+ const tcResult = `{"result":"tool output for message ${i}"}`;
459
+ events.push(
460
+ toolCallChunkEvent({
461
+ toolCallId: tcId,
462
+ toolCallName: "exampleTool",
463
+ parentMessageId: msgId,
464
+ delta: "",
465
+ }),
466
+ );
467
+ events.push(
468
+ toolCallChunkEvent({
469
+ toolCallId: tcId,
470
+ parentMessageId: msgId,
471
+ delta: tcResult,
472
+ }),
473
+ );
474
+ events.push(
475
+ toolCallResultEvent({
476
+ toolCallId: tcId,
477
+ messageId: `${msgId}-result`,
478
+ content: tcResult,
479
+ }),
480
+ );
481
+ }
482
+ }
483
+
484
+ return events;
485
+ }
486
+
397
487
  /**
398
488
  * Helper to emit a complete suggestion tool call with streaming chunks
399
489
  */
@@ -440,10 +530,20 @@ export function emitSuggestionToolCall(
440
530
  * A MockStepwiseAgent that emits suggestion events when run() is called
441
531
  */
442
532
  export class SuggestionsProviderAgent extends MockStepwiseAgent {
443
- private _suggestions: Array<{ title: string; message: string }> = [];
533
+ // Shared via a container so clone() and original see the same value even
534
+ // when setSuggestions() is called after the clone is created.
535
+ private _shared: { suggestions: Array<{ title: string; message: string }> } =
536
+ { suggestions: [] };
444
537
 
445
538
  setSuggestions(suggestions: Array<{ title: string; message: string }>) {
446
- this._suggestions = suggestions;
539
+ this._shared.suggestions = suggestions;
540
+ }
541
+
542
+ clone(): this {
543
+ const cloned = super.clone();
544
+ (cloned as unknown as { _shared: typeof this._shared })._shared =
545
+ this._shared;
546
+ return cloned;
447
547
  }
448
548
 
449
549
  run(_input: RunAgentInput): Observable<BaseEvent> {
@@ -458,7 +558,7 @@ export class SuggestionsProviderAgent extends MockStepwiseAgent {
458
558
  emitSuggestionToolCall(this, {
459
559
  toolCallId: testId("tc"),
460
560
  parentMessageId: messageId,
461
- suggestions: this._suggestions,
561
+ suggestions: this._shared.suggestions,
462
562
  });
463
563
 
464
564
  this.emit({ type: EventType.RUN_FINISHED } as BaseEvent);
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ import {
4
+ buildCatalogContextValue,
5
+ A2UI_SCHEMA_CONTEXT_DESCRIPTION,
6
+ extractCatalogComponentSchemas,
7
+ } from "@copilotkit/a2ui-renderer";
8
+ import {
9
+ A2UI_DEFAULT_GENERATION_GUIDELINES,
10
+ A2UI_DEFAULT_DESIGN_GUIDELINES,
11
+ } from "@copilotkit/shared";
12
+ import { useAgentContext } from "../hooks/use-agent-context";
13
+ import { useCopilotKit } from "../providers/CopilotKitProvider";
14
+ import { useLayoutEffect, useMemo } from "react";
15
+
16
+ /**
17
+ * Renders agent context describing the available A2UI catalog and custom components.
18
+ * Only mount this component when A2UI is enabled.
19
+ *
20
+ * When `includeSchema` is true, the full component schemas (JSON Schema) are also
21
+ * sent as context using the same description key as the A2UI middleware, so the
22
+ * middleware can optionally overwrite it with a server-side schema.
23
+ */
24
+ export function A2UICatalogContext({
25
+ catalog,
26
+ includeSchema,
27
+ }: {
28
+ catalog?: any;
29
+ includeSchema?: boolean;
30
+ }) {
31
+ const contextValue = buildCatalogContextValue(catalog);
32
+
33
+ useAgentContext({
34
+ description:
35
+ "A2UI catalog capabilities: available catalog IDs and custom component definitions the client can render.",
36
+ value: contextValue,
37
+ });
38
+
39
+ // When includeSchema is true, send full component schemas in the same format
40
+ // as the A2UI middleware so it can overwrite with a server-side schema if needed.
41
+ const { copilotkit } = useCopilotKit();
42
+ const schemaValue = useMemo(
43
+ () =>
44
+ includeSchema !== false
45
+ ? JSON.stringify(extractCatalogComponentSchemas(catalog))
46
+ : null,
47
+ [catalog, includeSchema],
48
+ );
49
+
50
+ useLayoutEffect(() => {
51
+ if (!copilotkit || !schemaValue) return;
52
+ const ids: string[] = [];
53
+ ids.push(
54
+ copilotkit.addContext({
55
+ description: A2UI_SCHEMA_CONTEXT_DESCRIPTION,
56
+ value: schemaValue,
57
+ }),
58
+ );
59
+ ids.push(
60
+ copilotkit.addContext({
61
+ description:
62
+ "A2UI generation guidelines — protocol rules, tool arguments, path rules, data model format, and form/two-way-binding instructions.",
63
+ value: A2UI_DEFAULT_GENERATION_GUIDELINES,
64
+ }),
65
+ );
66
+ ids.push(
67
+ copilotkit.addContext({
68
+ description:
69
+ "A2UI design guidelines — visual design rules, component hierarchy tips, and action handler patterns.",
70
+ value: A2UI_DEFAULT_DESIGN_GUIDELINES,
71
+ }),
72
+ );
73
+ return () => {
74
+ for (const id of ids) copilotkit.removeContext(id);
75
+ };
76
+ }, [copilotkit, schemaValue]);
77
+
78
+ return null;
79
+ }
@@ -5,6 +5,7 @@ import { z } from "zod";
5
5
  import {
6
6
  A2UIProvider,
7
7
  useA2UIActions,
8
+ useA2UIError,
8
9
  A2UIRenderer,
9
10
  initializeDefaultCatalog,
10
11
  injectStyles,
@@ -12,6 +13,12 @@ import {
12
13
  } from "@copilotkit/a2ui-renderer";
13
14
  import type { Theme, A2UIClientEventMessage } from "@copilotkit/a2ui-renderer";
14
15
 
16
+ /**
17
+ * The container key used to wrap A2UI operations for explicit detection.
18
+ * Must match A2UI_OPERATIONS_KEY in @ag-ui/a2ui-middleware and copilotkit.a2ui (Python).
19
+ */
20
+ const A2UI_OPERATIONS_KEY = "a2ui_operations";
21
+
15
22
  // Initialize the React renderer's component catalog and styles once
16
23
  let initialized = false;
17
24
  function ensureInitialized() {
@@ -22,14 +29,30 @@ function ensureInitialized() {
22
29
  }
23
30
  }
24
31
 
32
+ /**
33
+ * User action with dataContextPath, as dispatched by A2UI components.
34
+ */
35
+ export type A2UIUserAction = {
36
+ name: string;
37
+ sourceComponentId: string;
38
+ surfaceId: string;
39
+ timestamp: string;
40
+ context?: Record<string, unknown>;
41
+ dataContextPath?: string;
42
+ };
43
+
25
44
  export type A2UIMessageRendererOptions = {
26
45
  theme: Theme;
46
+ /** Optional component catalog to pass to A2UIProvider */
47
+ catalog?: any;
48
+ /** Optional custom loading component shown while A2UI surface is generating. */
49
+ loadingComponent?: React.ComponentType;
27
50
  };
28
51
 
29
52
  export function createA2UIMessageRenderer(
30
53
  options: A2UIMessageRendererOptions,
31
54
  ): ReactActivityMessageRenderer<any> {
32
- const { theme } = options;
55
+ const { theme, catalog, loadingComponent } = options;
33
56
 
34
57
  return {
35
58
  activityType: "a2ui-surface",
@@ -38,24 +61,20 @@ export function createA2UIMessageRenderer(
38
61
  ensureInitialized();
39
62
 
40
63
  const [operations, setOperations] = useState<any[]>([]);
41
- const lastSignatureRef = useRef<string | null>(null);
42
64
  const { copilotkit } = useCopilotKit();
43
65
 
66
+ const lastContentRef = useRef<unknown>(null);
44
67
  useEffect(() => {
45
- if (!content || !Array.isArray(content.operations)) {
46
- lastSignatureRef.current = null;
47
- setOperations([]);
48
- return;
49
- }
50
-
51
- const incoming = content.operations as any[];
52
- const signature = stringifyOperations(incoming);
68
+ // Skip if same content reference
69
+ if (content === lastContentRef.current) return;
70
+ lastContentRef.current = content;
53
71
 
54
- if (signature && signature === lastSignatureRef.current) {
72
+ const incoming = content?.[A2UI_OPERATIONS_KEY];
73
+ if (!content || !Array.isArray(incoming)) {
74
+ setOperations([]);
55
75
  return;
56
76
  }
57
77
 
58
- lastSignatureRef.current = signature;
59
78
  setOperations(incoming);
60
79
  }, [content]);
61
80
 
@@ -77,7 +96,9 @@ export function createA2UIMessageRenderer(
77
96
  }, [operations]);
78
97
 
79
98
  if (!groupedOperations.size) {
80
- return null;
99
+ // Show loading state while A2UI surface is being generated
100
+ const LoadingComponent = loadingComponent ?? DefaultA2UILoading;
101
+ return <LoadingComponent />;
81
102
  }
82
103
 
83
104
  return (
@@ -90,6 +111,7 @@ export function createA2UIMessageRenderer(
90
111
  theme={theme}
91
112
  agent={agent}
92
113
  copilotkit={copilotkit}
114
+ catalog={catalog}
93
115
  />
94
116
  ))}
95
117
  </div>
@@ -104,6 +126,8 @@ type ReactSurfaceHostProps = {
104
126
  theme: Theme;
105
127
  agent: any;
106
128
  copilotkit: any;
129
+ /** Optional component catalog to pass to A2UIProvider */
130
+ catalog?: any;
107
131
  };
108
132
 
109
133
  /**
@@ -116,15 +140,16 @@ function ReactSurfaceHost({
116
140
  theme,
117
141
  agent,
118
142
  copilotkit,
143
+ catalog,
119
144
  }: ReactSurfaceHostProps) {
120
- // Bridge: when the React renderer dispatches an action, send it to CopilotKit
145
+ // Bridge: when the React renderer dispatches an action, forward to CopilotKit
121
146
  const handleAction = useCallback(
122
147
  async (message: A2UIClientEventMessage) => {
123
148
  if (!agent) return;
124
149
 
125
- try {
126
- console.info("[A2UI] Action dispatched", message.userAction);
150
+ const action = message.userAction as A2UIUserAction | undefined;
127
151
 
152
+ try {
128
153
  copilotkit.setProperties({
129
154
  ...(copilotkit.properties ?? {}),
130
155
  a2uiAction: message,
@@ -143,17 +168,33 @@ function ReactSurfaceHost({
143
168
 
144
169
  return (
145
170
  <div className="cpk:flex cpk:w-full cpk:flex-none cpk:flex-col cpk:gap-4">
146
- <A2UIProvider onAction={handleAction} theme={theme}>
171
+ <A2UIProvider onAction={handleAction} theme={theme} catalog={catalog}>
147
172
  <SurfaceMessageProcessor
148
173
  surfaceId={surfaceId}
149
174
  operations={operations}
150
175
  />
151
- <A2UIRenderer surfaceId={surfaceId} className="cpk:flex cpk:flex-1" />
176
+ <A2UISurfaceOrError surfaceId={surfaceId} />
152
177
  </A2UIProvider>
153
178
  </div>
154
179
  );
155
180
  }
156
181
 
182
+ /**
183
+ * Renders the A2UI surface, or an error message if processing failed.
184
+ * Must be a child of A2UIProvider to access the error state.
185
+ */
186
+ function A2UISurfaceOrError({ surfaceId }: { surfaceId: string }) {
187
+ const error = useA2UIError();
188
+ if (error) {
189
+ return (
190
+ <div className="cpk:rounded-lg cpk:border cpk:border-red-200 cpk:bg-red-50 cpk:p-3 cpk:text-sm cpk:text-red-700">
191
+ A2UI render error: {error}
192
+ </div>
193
+ );
194
+ }
195
+ return <A2UIRenderer surfaceId={surfaceId} className="cpk:flex cpk:flex-1" />;
196
+ }
197
+
157
198
  /**
158
199
  * Processes A2UI operations into the provider's message processor.
159
200
  * Must be a child of A2UIProvider to access the actions context.
@@ -165,20 +206,74 @@ function SurfaceMessageProcessor({
165
206
  surfaceId: string;
166
207
  operations: any[];
167
208
  }) {
168
- const { processMessages } = useA2UIActions();
169
- const lastProcessedRef = useRef<string>("");
170
-
209
+ const { processMessages, getSurface } = useA2UIActions();
210
+ const lastHashRef = useRef<string>("");
171
211
  useEffect(() => {
172
- const key = `${surfaceId}-${JSON.stringify(operations)}`;
173
- if (key === lastProcessedRef.current) return;
174
- lastProcessedRef.current = key;
212
+ // Skip if operations haven't actually changed (deep compare via hash).
213
+ // ACTIVITY_DELTA + ACTIVITY_SNAPSHOT can trigger multiple renders with
214
+ // the same logical content but different object references.
215
+ const hash = JSON.stringify(operations);
216
+ if (hash === lastHashRef.current) return;
217
+ lastHashRef.current = hash;
175
218
 
176
- processMessages(operations);
177
- }, [processMessages, surfaceId, operations]);
219
+ // Filter out createSurface if the surface already exists — the
220
+ // MessageProcessor throws on duplicate createSurface, but content
221
+ // snapshots always include the full operation list.
222
+ const existing = getSurface(surfaceId);
223
+ const ops = existing
224
+ ? operations.filter((op) => !op?.createSurface)
225
+ : operations;
226
+
227
+ // Error handling is done inside A2UIProvider.processMessages
228
+ processMessages(ops);
229
+ }, [processMessages, getSurface, surfaceId, operations]);
178
230
 
179
231
  return null;
180
232
  }
181
233
 
234
+ /**
235
+ * Default loading component shown while an A2UI surface is generating.
236
+ * Displays an animated shimmer skeleton.
237
+ */
238
+ function DefaultA2UILoading() {
239
+ return (
240
+ <div
241
+ className="cpk:flex cpk:flex-col cpk:gap-3 cpk:rounded-xl cpk:border cpk:border-gray-100 cpk:bg-gray-50/50 cpk:p-5"
242
+ style={{ minHeight: 120 }}
243
+ >
244
+ <div className="cpk:flex cpk:items-center cpk:gap-2">
245
+ <div
246
+ className="cpk:h-3 cpk:w-3 cpk:rounded-full cpk:bg-gray-200"
247
+ style={{
248
+ animation: "cpk-a2ui-pulse 1.5s ease-in-out infinite",
249
+ }}
250
+ />
251
+ <span className="cpk:text-xs cpk:font-medium cpk:text-gray-400">
252
+ Generating UI...
253
+ </span>
254
+ </div>
255
+ <div className="cpk:flex cpk:flex-col cpk:gap-2">
256
+ {[0.8, 0.6, 0.4].map((width, i) => (
257
+ <div
258
+ key={i}
259
+ className="cpk:h-3 cpk:rounded cpk:bg-gray-200/70"
260
+ style={{
261
+ width: `${width * 100}%`,
262
+ animation: `cpk-a2ui-pulse 1.5s ease-in-out ${i * 0.15}s infinite`,
263
+ }}
264
+ />
265
+ ))}
266
+ </div>
267
+ <style>{`
268
+ @keyframes cpk-a2ui-pulse {
269
+ 0%, 100% { opacity: 0.4; }
270
+ 50% { opacity: 1; }
271
+ }
272
+ `}</style>
273
+ </div>
274
+ );
275
+ }
276
+
182
277
  function getOperationSurfaceId(operation: any): string | null {
183
278
  if (!operation || typeof operation !== "object") {
184
279
  return null;
@@ -188,19 +283,12 @@ function getOperationSurfaceId(operation: any): string | null {
188
283
  return operation.surfaceId;
189
284
  }
190
285
 
286
+ // v0.9 message keys
191
287
  return (
192
- operation?.beginRendering?.surfaceId ??
193
- operation?.surfaceUpdate?.surfaceId ??
194
- operation?.dataModelUpdate?.surfaceId ??
288
+ operation?.createSurface?.surfaceId ??
289
+ operation?.updateComponents?.surfaceId ??
290
+ operation?.updateDataModel?.surfaceId ??
195
291
  operation?.deleteSurface?.surfaceId ??
196
292
  null
197
293
  );
198
294
  }
199
-
200
- function stringifyOperations(ops: any[]): string | null {
201
- try {
202
- return JSON.stringify(ops);
203
- } catch (error) {
204
- return null;
205
- }
206
- }