@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
@@ -164,6 +164,7 @@ export function renderWithCopilotKit({
164
164
  humanInTheLoop,
165
165
  agentId,
166
166
  threadId,
167
+ defaultThrottleMs,
167
168
  children,
168
169
  }: {
169
170
  agent?: AbstractAgent;
@@ -175,6 +176,7 @@ export function renderWithCopilotKit({
175
176
  humanInTheLoop?: any[];
176
177
  agentId?: string;
177
178
  threadId?: string;
179
+ defaultThrottleMs?: number;
178
180
  children?: React.ReactNode;
179
181
  }): ReturnType<typeof render> {
180
182
  const resolvedAgents = agents || (agent ? { default: agent } : undefined);
@@ -189,6 +191,7 @@ export function renderWithCopilotKit({
189
191
  renderActivityMessages={renderActivityMessages}
190
192
  frontendTools={frontendTools}
191
193
  humanInTheLoop={humanInTheLoop}
194
+ defaultThrottleMs={defaultThrottleMs}
192
195
  >
193
196
  <CopilotChatConfigurationProvider
194
197
  agentId={resolvedAgentId}
@@ -417,6 +420,70 @@ export function testId(prefix: string): string {
417
420
  return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
418
421
  }
419
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
+
420
487
  /**
421
488
  * Helper to emit a complete suggestion tool call with streaming chunks
422
489
  */
@@ -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
- }