@copilotkit/react-core 1.54.1 → 1.55.0-next.8

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 (183) hide show
  1. package/CHANGELOG.md +127 -116
  2. package/dist/copilotkit-B3Mb1yVE.cjs +7975 -0
  3. package/dist/copilotkit-B3Mb1yVE.cjs.map +1 -0
  4. package/dist/copilotkit-DBzgOMby.d.cts +2182 -0
  5. package/dist/copilotkit-DBzgOMby.d.cts.map +1 -0
  6. package/dist/copilotkit-DNYSFuz5.mjs +7562 -0
  7. package/dist/copilotkit-DNYSFuz5.mjs.map +1 -0
  8. package/dist/copilotkit-Dy5w3qEV.d.mts +2182 -0
  9. package/dist/copilotkit-Dy5w3qEV.d.mts.map +1 -0
  10. package/dist/index.cjs +27 -28
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +3 -3
  13. package/dist/index.d.cts.map +1 -1
  14. package/dist/index.d.mts +3 -3
  15. package/dist/index.d.mts.map +1 -1
  16. package/dist/index.mjs +4 -5
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/index.umd.js +1941 -35
  19. package/dist/index.umd.js.map +1 -1
  20. package/dist/v2/index.cjs +77 -7
  21. package/dist/v2/index.css +1 -2
  22. package/dist/v2/index.d.cts +6 -4
  23. package/dist/v2/index.d.mts +6 -4
  24. package/dist/v2/index.mjs +7 -4
  25. package/dist/v2/index.umd.js +5725 -24
  26. package/dist/v2/index.umd.js.map +1 -1
  27. package/package.json +37 -9
  28. package/scripts/scope-preflight.mjs +101 -0
  29. package/src/components/CopilotListeners.tsx +2 -6
  30. package/src/components/copilot-provider/copilot-messages.tsx +1 -1
  31. package/src/components/copilot-provider/copilotkit-props.tsx +1 -1
  32. package/src/components/copilot-provider/copilotkit.tsx +4 -4
  33. package/src/context/copilot-messages-context.tsx +1 -1
  34. package/src/hooks/__tests__/use-coagent-config.test.ts +2 -2
  35. package/src/hooks/__tests__/use-coagent-state-render.e2e.test.tsx +2 -2
  36. package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +3 -7
  37. package/src/hooks/__tests__/use-frontend-tool-available.test.tsx +1 -1
  38. package/src/hooks/__tests__/use-frontend-tool-remount.e2e.test.tsx +4 -4
  39. package/src/hooks/use-agent-nodename.ts +1 -1
  40. package/src/hooks/use-coagent-state-render-bridge.tsx +1 -4
  41. package/src/hooks/use-coagent.ts +1 -1
  42. package/src/hooks/use-configure-chat-suggestions.tsx +2 -2
  43. package/src/hooks/use-copilot-chat-suggestions.tsx +2 -2
  44. package/src/hooks/use-copilot-chat_internal.ts +2 -2
  45. package/src/hooks/use-copilot-readable.ts +1 -1
  46. package/src/hooks/use-frontend-tool.ts +2 -2
  47. package/src/hooks/use-human-in-the-loop.ts +2 -2
  48. package/src/hooks/use-langgraph-interrupt.ts +2 -5
  49. package/src/hooks/use-lazy-tool-renderer.tsx +1 -1
  50. package/src/hooks/use-render-tool-call.ts +1 -1
  51. package/src/lib/copilot-task.ts +1 -1
  52. package/src/setupTests.ts +18 -14
  53. package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +176 -0
  54. package/src/v2/__tests__/globalSetup.ts +14 -0
  55. package/src/v2/__tests__/setup.ts +93 -0
  56. package/src/v2/__tests__/utils/test-helpers.tsx +470 -0
  57. package/src/v2/a2ui/A2UIMessageRenderer.tsx +206 -0
  58. package/src/v2/components/CopilotKitInspector.tsx +50 -0
  59. package/src/v2/components/MCPAppsActivityRenderer.tsx +785 -0
  60. package/src/v2/components/WildcardToolCallRender.tsx +86 -0
  61. package/src/v2/components/__tests__/license-warning-banner.test.tsx +46 -0
  62. package/src/v2/components/chat/CopilotChat.tsx +431 -0
  63. package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +375 -0
  64. package/src/v2/components/chat/CopilotChatAudioRecorder.tsx +350 -0
  65. package/src/v2/components/chat/CopilotChatInput.tsx +1302 -0
  66. package/src/v2/components/chat/CopilotChatMessageView.tsx +556 -0
  67. package/src/v2/components/chat/CopilotChatReasoningMessage.tsx +252 -0
  68. package/src/v2/components/chat/CopilotChatSuggestionPill.tsx +59 -0
  69. package/src/v2/components/chat/CopilotChatSuggestionView.tsx +133 -0
  70. package/src/v2/components/chat/CopilotChatToggleButton.tsx +171 -0
  71. package/src/v2/components/chat/CopilotChatToolCallsView.tsx +40 -0
  72. package/src/v2/components/chat/CopilotChatUserMessage.tsx +388 -0
  73. package/src/v2/components/chat/CopilotChatView.tsx +598 -0
  74. package/src/v2/components/chat/CopilotModalHeader.tsx +129 -0
  75. package/src/v2/components/chat/CopilotPopup.tsx +81 -0
  76. package/src/v2/components/chat/CopilotPopupView.tsx +317 -0
  77. package/src/v2/components/chat/CopilotSidebar.tsx +76 -0
  78. package/src/v2/components/chat/CopilotSidebarView.tsx +255 -0
  79. package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.tsx +1113 -0
  80. package/src/v2/components/chat/__tests__/CopilotChat.onError.test.tsx +73 -0
  81. package/src/v2/components/chat/__tests__/CopilotChat.slots.e2e.test.tsx +432 -0
  82. package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +150 -0
  83. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.slots.e2e.test.tsx +624 -0
  84. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.tsx +702 -0
  85. package/src/v2/components/chat/__tests__/CopilotChatCssClasses.test.tsx +107 -0
  86. package/src/v2/components/chat/__tests__/CopilotChatInput.slots.e2e.test.tsx +929 -0
  87. package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +986 -0
  88. package/src/v2/components/chat/__tests__/CopilotChatMessageView.slots.e2e.test.tsx +1004 -0
  89. package/src/v2/components/chat/__tests__/CopilotChatMessageView.test.tsx +169 -0
  90. package/src/v2/components/chat/__tests__/CopilotChatSuggestionView.slots.e2e.test.tsx +530 -0
  91. package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.tsx +782 -0
  92. package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +2413 -0
  93. package/src/v2/components/chat/__tests__/CopilotChatUserMessage.slots.e2e.test.tsx +621 -0
  94. package/src/v2/components/chat/__tests__/CopilotChatView.onClick.e2e.test.tsx +853 -0
  95. package/src/v2/components/chat/__tests__/CopilotChatView.slots.e2e.test.tsx +1050 -0
  96. package/src/v2/components/chat/__tests__/CopilotModalHeader.slots.e2e.test.tsx +484 -0
  97. package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.tsx +612 -0
  98. package/src/v2/components/chat/__tests__/CopilotSidebarView.slots.e2e.test.tsx +502 -0
  99. package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +1011 -0
  100. package/src/v2/components/chat/__tests__/setup.ts +1 -0
  101. package/src/v2/components/chat/index.ts +79 -0
  102. package/src/v2/components/index.ts +7 -0
  103. package/src/v2/components/license-warning-banner.tsx +198 -0
  104. package/src/v2/components/ui/button.tsx +123 -0
  105. package/src/v2/components/ui/dropdown-menu.tsx +258 -0
  106. package/src/v2/components/ui/tooltip.tsx +60 -0
  107. package/src/v2/hooks/__tests__/standard-schema-types.test.tsx +152 -0
  108. package/src/v2/hooks/__tests__/standard-schema.test.tsx +282 -0
  109. package/src/v2/hooks/__tests__/use-agent-context-timing.e2e.test.tsx +132 -0
  110. package/src/v2/hooks/__tests__/use-agent-context.test.tsx +401 -0
  111. package/src/v2/hooks/__tests__/use-agent-error-state.test.tsx +44 -0
  112. package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +205 -0
  113. package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +148 -0
  114. package/src/v2/hooks/__tests__/use-component.test.tsx +123 -0
  115. package/src/v2/hooks/__tests__/use-configure-suggestions.e2e.test.tsx +696 -0
  116. package/src/v2/hooks/__tests__/use-default-render-tool.test.tsx +153 -0
  117. package/src/v2/hooks/__tests__/use-frontend-tool-available.test.tsx +167 -0
  118. package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +2129 -0
  119. package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.tsx +1261 -0
  120. package/src/v2/hooks/__tests__/use-interrupt.test.tsx +397 -0
  121. package/src/v2/hooks/__tests__/use-katex-styles.test.tsx +56 -0
  122. package/src/v2/hooks/__tests__/use-keyboard-height.test.tsx +192 -0
  123. package/src/v2/hooks/__tests__/use-render-tool.test.tsx +259 -0
  124. package/src/v2/hooks/__tests__/use-suggestions.e2e.test.tsx +524 -0
  125. package/src/v2/hooks/__tests__/use-threads.test.tsx +433 -0
  126. package/src/v2/hooks/__tests__/zod-regression.test.tsx +311 -0
  127. package/src/v2/hooks/index.ts +18 -0
  128. package/src/v2/hooks/use-agent-context.tsx +45 -0
  129. package/src/v2/hooks/use-agent.tsx +155 -0
  130. package/src/v2/hooks/use-component.tsx +89 -0
  131. package/src/v2/hooks/use-configure-suggestions.tsx +187 -0
  132. package/src/v2/hooks/use-default-render-tool.tsx +254 -0
  133. package/src/v2/hooks/use-frontend-tool.tsx +43 -0
  134. package/src/v2/hooks/use-human-in-the-loop.tsx +81 -0
  135. package/src/v2/hooks/use-interrupt.tsx +305 -0
  136. package/src/v2/hooks/use-keyboard-height.tsx +67 -0
  137. package/src/v2/hooks/use-render-activity-message.tsx +73 -0
  138. package/src/v2/hooks/use-render-custom-messages.tsx +93 -0
  139. package/src/v2/hooks/use-render-tool-call.tsx +175 -0
  140. package/src/v2/hooks/use-render-tool.tsx +181 -0
  141. package/src/v2/hooks/use-suggestions.tsx +91 -0
  142. package/src/v2/hooks/use-threads.tsx +256 -0
  143. package/src/v2/hooks/useKatexStyles.ts +27 -0
  144. package/src/v2/index.css +1 -1
  145. package/src/v2/index.ts +18 -2
  146. package/src/v2/lib/__tests__/completePartialMarkdown.test.ts +495 -0
  147. package/src/v2/lib/__tests__/renderSlot.test.tsx +588 -0
  148. package/src/v2/lib/react-core.ts +156 -0
  149. package/src/v2/lib/slots.tsx +143 -0
  150. package/src/v2/lib/transcription-client.ts +184 -0
  151. package/src/v2/lib/utils.ts +8 -0
  152. package/src/v2/providers/CopilotChatConfigurationProvider.tsx +162 -0
  153. package/src/v2/providers/CopilotKitProvider.tsx +600 -0
  154. package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.tsx +546 -0
  155. package/src/v2/providers/__tests__/CopilotKitProvider.license.test.tsx +101 -0
  156. package/src/v2/providers/__tests__/CopilotKitProvider.onError.test.tsx +69 -0
  157. package/src/v2/providers/__tests__/CopilotKitProvider.renderCustomMessages.e2e.test.tsx +881 -0
  158. package/src/v2/providers/__tests__/CopilotKitProvider.stability.test.tsx +740 -0
  159. package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +642 -0
  160. package/src/v2/providers/__tests__/CopilotKitProvider.wildcard.test.tsx +294 -0
  161. package/src/v2/providers/index.ts +14 -0
  162. package/src/v2/styles/globals.css +230 -0
  163. package/src/v2/types/__tests__/defineToolCallRenderer.test.tsx +525 -0
  164. package/src/v2/types/defineToolCallRenderer.ts +65 -0
  165. package/src/v2/types/frontend-tool.ts +8 -0
  166. package/src/v2/types/human-in-the-loop.ts +33 -0
  167. package/src/v2/types/index.ts +7 -0
  168. package/src/v2/types/interrupt.ts +15 -0
  169. package/src/v2/types/react-activity-message-renderer.ts +27 -0
  170. package/src/v2/types/react-custom-message-renderer.ts +17 -0
  171. package/src/v2/types/react-tool-call-renderer.ts +32 -0
  172. package/tsdown.config.ts +34 -10
  173. package/vitest.config.mjs +4 -3
  174. package/LICENSE +0 -21
  175. package/dist/copilotkit-BRPQ2sqS.d.cts +0 -670
  176. package/dist/copilotkit-BRPQ2sqS.d.cts.map +0 -1
  177. package/dist/copilotkit-C94ayZbs.cjs +0 -2161
  178. package/dist/copilotkit-C94ayZbs.cjs.map +0 -1
  179. package/dist/copilotkit-CwZMFmSK.d.mts +0 -670
  180. package/dist/copilotkit-CwZMFmSK.d.mts.map +0 -1
  181. package/dist/copilotkit-Yh_Ld_FX.mjs +0 -2031
  182. package/dist/copilotkit-Yh_Ld_FX.mjs.map +0 -1
  183. package/dist/v2/index.css.map +0 -1
@@ -0,0 +1,785 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useRef, useState, useCallback } from "react";
4
+ import { z } from "zod";
5
+ import type { AbstractAgent, RunAgentResult } from "@ag-ui/client";
6
+
7
+ // Protocol version supported
8
+ const PROTOCOL_VERSION = "2025-06-18";
9
+
10
+ // Build sandbox proxy HTML with optional extra CSP domains from resource metadata
11
+ function buildSandboxHTML(extraCspDomains?: string[]): string {
12
+ const baseScriptSrc =
13
+ "'self' 'wasm-unsafe-eval' 'unsafe-inline' 'unsafe-eval' blob: data: http://localhost:* https://localhost:*";
14
+ const baseFrameSrc = "* blob: data: http://localhost:* https://localhost:*";
15
+ const extra = extraCspDomains?.length ? " " + extraCspDomains.join(" ") : "";
16
+ const scriptSrc = baseScriptSrc + extra;
17
+ const frameSrc = baseFrameSrc + extra;
18
+
19
+ return `<!doctype html>
20
+ <html>
21
+ <head>
22
+ <meta charset="utf-8" />
23
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src * data: blob: 'unsafe-inline'; media-src * blob: data:; font-src * blob: data:; script-src ${scriptSrc}; style-src * blob: data: 'unsafe-inline'; connect-src *; frame-src ${frameSrc}; base-uri 'self';" />
24
+ <style>html,body{margin:0;padding:0;height:100%;width:100%;overflow:hidden}*{box-sizing:border-box}iframe{background-color:transparent;border:none;padding:0;overflow:hidden;width:100%;height:100%}</style>
25
+ </head>
26
+ <body>
27
+ <script>
28
+ if(window.self===window.top){throw new Error("This file must be used in an iframe.")}
29
+ const inner=document.createElement("iframe");
30
+ inner.style="width:100%;height:100%;border:none;";
31
+ inner.setAttribute("sandbox","allow-scripts allow-same-origin allow-forms");
32
+ document.body.appendChild(inner);
33
+ window.addEventListener("message",async(event)=>{
34
+ if(event.source===window.parent){
35
+ if(event.data&&event.data.method==="ui/notifications/sandbox-resource-ready"){
36
+ const{html,sandbox}=event.data.params;
37
+ if(typeof sandbox==="string")inner.setAttribute("sandbox",sandbox);
38
+ if(typeof html==="string")inner.srcdoc=html;
39
+ }else if(inner&&inner.contentWindow){
40
+ inner.contentWindow.postMessage(event.data,"*");
41
+ }
42
+ }else if(event.source===inner.contentWindow){
43
+ window.parent.postMessage(event.data,"*");
44
+ }
45
+ });
46
+ window.parent.postMessage({jsonrpc:"2.0",method:"ui/notifications/sandbox-proxy-ready",params:{}},"*");
47
+ </script>
48
+ </body>
49
+ </html>`;
50
+ }
51
+
52
+ /**
53
+ * Queue for serializing MCP app requests to an agent.
54
+ * Ensures requests wait for the agent to stop running and are processed one at a time.
55
+ */
56
+ class MCPAppsRequestQueue {
57
+ private queues = new Map<
58
+ string,
59
+ Array<{
60
+ execute: () => Promise<RunAgentResult>;
61
+ resolve: (result: RunAgentResult) => void;
62
+ reject: (error: Error) => void;
63
+ }>
64
+ >();
65
+ private processing = new Map<string, boolean>();
66
+
67
+ /**
68
+ * Add a request to the queue for a specific agent thread.
69
+ * Returns a promise that resolves when the request completes.
70
+ */
71
+ async enqueue(
72
+ agent: AbstractAgent,
73
+ request: () => Promise<RunAgentResult>,
74
+ ): Promise<RunAgentResult> {
75
+ const threadId = agent.threadId || "default";
76
+
77
+ return new Promise((resolve, reject) => {
78
+ // Get or create queue for this thread
79
+ let queue = this.queues.get(threadId);
80
+ if (!queue) {
81
+ queue = [];
82
+ this.queues.set(threadId, queue);
83
+ }
84
+
85
+ // Add request to queue
86
+ queue.push({ execute: request, resolve, reject });
87
+
88
+ // Start processing if not already running
89
+ this.processQueue(threadId, agent);
90
+ });
91
+ }
92
+
93
+ private async processQueue(
94
+ threadId: string,
95
+ agent: AbstractAgent,
96
+ ): Promise<void> {
97
+ // If already processing this queue, return
98
+ if (this.processing.get(threadId)) {
99
+ return;
100
+ }
101
+
102
+ this.processing.set(threadId, true);
103
+
104
+ try {
105
+ const queue = this.queues.get(threadId);
106
+ if (!queue) return;
107
+
108
+ while (queue.length > 0) {
109
+ const item = queue[0]!;
110
+
111
+ try {
112
+ // Wait for any active run to complete before processing
113
+ await this.waitForAgentIdle(agent);
114
+
115
+ // Execute the request
116
+ const result = await item.execute();
117
+ item.resolve(result);
118
+ } catch (error) {
119
+ item.reject(
120
+ error instanceof Error ? error : new Error(String(error)),
121
+ );
122
+ }
123
+
124
+ // Remove processed item
125
+ queue.shift();
126
+ }
127
+ } finally {
128
+ this.processing.set(threadId, false);
129
+ }
130
+ }
131
+
132
+ private waitForAgentIdle(agent: AbstractAgent): Promise<void> {
133
+ return new Promise((resolve) => {
134
+ if (!agent.isRunning) {
135
+ resolve();
136
+ return;
137
+ }
138
+
139
+ let done = false;
140
+ const finish = () => {
141
+ if (done) return;
142
+ done = true;
143
+ clearInterval(checkInterval);
144
+ sub.unsubscribe();
145
+ resolve();
146
+ };
147
+
148
+ const sub = agent.subscribe({
149
+ onRunFinalized: finish,
150
+ onRunFailed: finish,
151
+ });
152
+
153
+ // Fallback for reconnect scenarios where events don't fire
154
+ const checkInterval = setInterval(() => {
155
+ if (!agent.isRunning) finish();
156
+ }, 500);
157
+ });
158
+ }
159
+ }
160
+
161
+ // Global queue instance for all MCP app requests
162
+ const mcpAppsRequestQueue = new MCPAppsRequestQueue();
163
+
164
+ /**
165
+ * Activity type for MCP Apps events - must match the middleware's MCPAppsActivityType
166
+ */
167
+ export const MCPAppsActivityType = "mcp-apps";
168
+
169
+ // Zod schema for activity content validation (middleware 0.0.2 format)
170
+ export const MCPAppsActivityContentSchema = z.object({
171
+ result: z.object({
172
+ content: z.array(z.any()).optional(),
173
+ structuredContent: z.any().optional(),
174
+ isError: z.boolean().optional(),
175
+ }),
176
+ // Resource URI to fetch (e.g., "ui://server/dashboard")
177
+ resourceUri: z.string(),
178
+ // MD5 hash of server config (renamed from serverId in 0.0.1)
179
+ serverHash: z.string(),
180
+ // Optional stable server ID from config (takes precedence over serverHash)
181
+ serverId: z.string().optional(),
182
+ // Original tool input arguments
183
+ toolInput: z.record(z.unknown()).optional(),
184
+ });
185
+
186
+ export type MCPAppsActivityContent = z.infer<
187
+ typeof MCPAppsActivityContentSchema
188
+ >;
189
+
190
+ // Type for the resource fetched from the server
191
+ interface FetchedResource {
192
+ uri: string;
193
+ mimeType?: string;
194
+ text?: string;
195
+ blob?: string;
196
+ _meta?: {
197
+ ui?: {
198
+ prefersBorder?: boolean;
199
+ csp?: {
200
+ connectDomains?: string[];
201
+ resourceDomains?: string[];
202
+ };
203
+ };
204
+ };
205
+ }
206
+
207
+ interface JSONRPCRequest {
208
+ jsonrpc: "2.0";
209
+ id: string | number;
210
+ method: string;
211
+ params?: Record<string, unknown>;
212
+ }
213
+
214
+ interface JSONRPCResponse {
215
+ jsonrpc: "2.0";
216
+ id: string | number;
217
+ result?: unknown;
218
+ error?: { code: number; message: string };
219
+ }
220
+
221
+ interface JSONRPCNotification {
222
+ jsonrpc: "2.0";
223
+ method: string;
224
+ params?: Record<string, unknown>;
225
+ }
226
+
227
+ type JSONRPCMessage = JSONRPCRequest | JSONRPCResponse | JSONRPCNotification;
228
+
229
+ function isRequest(msg: JSONRPCMessage): msg is JSONRPCRequest {
230
+ return "id" in msg && "method" in msg;
231
+ }
232
+
233
+ function isNotification(msg: JSONRPCMessage): msg is JSONRPCNotification {
234
+ return !("id" in msg) && "method" in msg;
235
+ }
236
+
237
+ /**
238
+ * Props for the activity renderer component
239
+ */
240
+ interface MCPAppsActivityRendererProps {
241
+ activityType: string;
242
+ content: MCPAppsActivityContent;
243
+ message: unknown; // ActivityMessage from @ag-ui/core
244
+ agent: AbstractAgent | undefined;
245
+ }
246
+
247
+ /**
248
+ * MCP Apps Extension Activity Renderer
249
+ *
250
+ * Renders MCP Apps UI in a sandboxed iframe with full protocol support.
251
+ * Fetches resource content on-demand via proxied MCP requests.
252
+ */
253
+ export const MCPAppsActivityRenderer: React.FC<MCPAppsActivityRendererProps> =
254
+ function MCPAppsActivityRenderer({ content, agent }) {
255
+ const containerRef = useRef<HTMLDivElement>(null);
256
+ const iframeRef = useRef<HTMLIFrameElement | null>(null);
257
+ const [iframeReady, setIframeReady] = useState(false);
258
+ const [error, setError] = useState<Error | null>(null);
259
+ const [isLoading, setIsLoading] = useState(true);
260
+ const [iframeSize, setIframeSize] = useState<{
261
+ width?: number;
262
+ height?: number;
263
+ }>({});
264
+ const [fetchedResource, setFetchedResource] =
265
+ useState<FetchedResource | null>(null);
266
+
267
+ // Use refs for values that shouldn't trigger re-renders but need latest values
268
+ const contentRef = useRef(content);
269
+ contentRef.current = content;
270
+
271
+ // Store agent in a ref for use in async handlers
272
+ const agentRef = useRef(agent);
273
+ agentRef.current = agent;
274
+
275
+ // Ref to track fetch state - survives StrictMode remounts
276
+ const fetchStateRef = useRef<{
277
+ inProgress: boolean;
278
+ promise: Promise<FetchedResource | null> | null;
279
+ resourceUri: string | null;
280
+ }>({ inProgress: false, promise: null, resourceUri: null });
281
+
282
+ // Callback to send a message to the iframe
283
+ const sendToIframe = useCallback((msg: JSONRPCMessage) => {
284
+ if (iframeRef.current?.contentWindow) {
285
+ console.log("[MCPAppsRenderer] Sending to iframe:", msg);
286
+ iframeRef.current.contentWindow.postMessage(msg, "*");
287
+ }
288
+ }, []);
289
+
290
+ // Callback to send a JSON-RPC response
291
+ const sendResponse = useCallback(
292
+ (id: string | number, result: unknown) => {
293
+ sendToIframe({
294
+ jsonrpc: "2.0",
295
+ id,
296
+ result,
297
+ });
298
+ },
299
+ [sendToIframe],
300
+ );
301
+
302
+ // Callback to send a JSON-RPC error response
303
+ const sendErrorResponse = useCallback(
304
+ (id: string | number, code: number, message: string) => {
305
+ sendToIframe({
306
+ jsonrpc: "2.0",
307
+ id,
308
+ error: { code, message },
309
+ });
310
+ },
311
+ [sendToIframe],
312
+ );
313
+
314
+ // Callback to send a notification
315
+ const sendNotification = useCallback(
316
+ (method: string, params?: Record<string, unknown>) => {
317
+ sendToIframe({
318
+ jsonrpc: "2.0",
319
+ method,
320
+ params: params || {},
321
+ });
322
+ },
323
+ [sendToIframe],
324
+ );
325
+
326
+ // Effect 0: Fetch the resource content on mount
327
+ // Uses ref-based deduplication to handle React StrictMode double-mounting
328
+ useEffect(() => {
329
+ const { resourceUri, serverHash, serverId } = content;
330
+
331
+ // Check if we already have a fetch in progress for this resource
332
+ // This handles StrictMode double-mounting - second mount reuses first mount's promise
333
+ if (
334
+ fetchStateRef.current.inProgress &&
335
+ fetchStateRef.current.resourceUri === resourceUri
336
+ ) {
337
+ // Reuse the existing promise
338
+ fetchStateRef.current.promise
339
+ ?.then((resource) => {
340
+ if (resource) {
341
+ setFetchedResource(resource);
342
+ setIsLoading(false);
343
+ }
344
+ })
345
+ .catch((err) => {
346
+ setError(err instanceof Error ? err : new Error(String(err)));
347
+ setIsLoading(false);
348
+ });
349
+ return;
350
+ }
351
+
352
+ if (!agent) {
353
+ setError(new Error("No agent available to fetch resource"));
354
+ setIsLoading(false);
355
+ return;
356
+ }
357
+
358
+ // Mark fetch as in progress
359
+ fetchStateRef.current.inProgress = true;
360
+ fetchStateRef.current.resourceUri = resourceUri;
361
+
362
+ // Create the fetch promise using the queue to serialize requests
363
+ const fetchPromise = (async (): Promise<FetchedResource | null> => {
364
+ try {
365
+ // Use queue to wait for agent to be idle and serialize requests
366
+ const runResult = await mcpAppsRequestQueue.enqueue(agent, () =>
367
+ agent.runAgent({
368
+ forwardedProps: {
369
+ __proxiedMCPRequest: {
370
+ serverHash,
371
+ serverId, // optional, takes precedence if provided
372
+ method: "resources/read",
373
+ params: { uri: resourceUri },
374
+ },
375
+ },
376
+ }),
377
+ );
378
+
379
+ // Extract resource from result
380
+ // The response format is: { contents: [{ uri, mimeType, text?, blob?, _meta? }] }
381
+ const resultData = runResult.result as
382
+ | { contents?: FetchedResource[] }
383
+ | undefined;
384
+ const resource = resultData?.contents?.[0];
385
+
386
+ if (!resource) {
387
+ throw new Error("No resource content in response");
388
+ }
389
+
390
+ return resource;
391
+ } catch (err) {
392
+ console.error("[MCPAppsRenderer] Failed to fetch resource:", err);
393
+ throw err;
394
+ } finally {
395
+ // Mark fetch as complete
396
+ fetchStateRef.current.inProgress = false;
397
+ }
398
+ })();
399
+
400
+ // Store the promise for potential reuse
401
+ fetchStateRef.current.promise = fetchPromise;
402
+
403
+ // Handle the result
404
+ fetchPromise
405
+ .then((resource) => {
406
+ if (resource) {
407
+ setFetchedResource(resource);
408
+ setIsLoading(false);
409
+ }
410
+ })
411
+ .catch((err) => {
412
+ setError(err instanceof Error ? err : new Error(String(err)));
413
+ setIsLoading(false);
414
+ });
415
+
416
+ // No cleanup needed - we want the fetch to complete even if StrictMode unmounts
417
+ }, [agent, content]);
418
+
419
+ // Effect 1: Setup sandbox proxy iframe and communication (after resource is fetched)
420
+ useEffect(() => {
421
+ // Wait for resource to be fetched
422
+ if (isLoading || !fetchedResource) {
423
+ return;
424
+ }
425
+
426
+ // Capture container reference at effect start (refs are cleared during unmount)
427
+ const container = containerRef.current;
428
+ if (!container) {
429
+ return;
430
+ }
431
+
432
+ let mounted = true;
433
+ let messageHandler: ((event: MessageEvent) => void) | null = null;
434
+ let initialListener: ((event: MessageEvent) => void) | null = null;
435
+ let createdIframe: HTMLIFrameElement | null = null;
436
+
437
+ const setup = async () => {
438
+ try {
439
+ // Create sandbox proxy iframe
440
+ const iframe = document.createElement("iframe");
441
+ createdIframe = iframe; // Track for cleanup
442
+ iframe.style.width = "100%";
443
+ iframe.style.height = "100px"; // Start small, will be resized by size-changed notification
444
+ iframe.style.border = "none";
445
+ iframe.style.backgroundColor = "transparent";
446
+ iframe.style.display = "block";
447
+ iframe.setAttribute(
448
+ "sandbox",
449
+ "allow-scripts allow-same-origin allow-forms",
450
+ );
451
+
452
+ // Wait for sandbox proxy to be ready
453
+ const sandboxReady = new Promise<void>((resolve) => {
454
+ initialListener = (event: MessageEvent) => {
455
+ if (event.source === iframe.contentWindow) {
456
+ if (
457
+ event.data?.method === "ui/notifications/sandbox-proxy-ready"
458
+ ) {
459
+ if (initialListener) {
460
+ window.removeEventListener("message", initialListener);
461
+ initialListener = null;
462
+ }
463
+ resolve();
464
+ }
465
+ }
466
+ };
467
+ window.addEventListener("message", initialListener);
468
+ });
469
+
470
+ // Check mounted before adding to DOM (handles StrictMode double-mount)
471
+ if (!mounted) {
472
+ if (initialListener) {
473
+ window.removeEventListener("message", initialListener);
474
+ initialListener = null;
475
+ }
476
+ return;
477
+ }
478
+
479
+ // Build sandbox HTML with CSP domains from resource metadata
480
+ const cspDomains = fetchedResource._meta?.ui?.csp?.resourceDomains;
481
+ iframe.srcdoc = buildSandboxHTML(cspDomains);
482
+ iframeRef.current = iframe;
483
+ container.appendChild(iframe);
484
+
485
+ // Wait for sandbox proxy to signal ready
486
+ await sandboxReady;
487
+ if (!mounted) return;
488
+
489
+ console.log("[MCPAppsRenderer] Sandbox proxy ready");
490
+
491
+ // Setup message handler for JSON-RPC messages from the inner iframe
492
+ messageHandler = async (event: MessageEvent) => {
493
+ if (event.source !== iframe.contentWindow) return;
494
+
495
+ const msg = event.data as JSONRPCMessage;
496
+ if (!msg || typeof msg !== "object" || msg.jsonrpc !== "2.0")
497
+ return;
498
+
499
+ console.log("[MCPAppsRenderer] Received from iframe:", msg);
500
+
501
+ // Handle requests (need response)
502
+ if (isRequest(msg)) {
503
+ switch (msg.method) {
504
+ case "ui/initialize": {
505
+ // Respond with host capabilities
506
+ sendResponse(msg.id, {
507
+ protocolVersion: PROTOCOL_VERSION,
508
+ hostInfo: {
509
+ name: "CopilotKit MCP Apps Host",
510
+ version: "1.0.0",
511
+ },
512
+ hostCapabilities: {
513
+ openLinks: {},
514
+ logging: {},
515
+ },
516
+ hostContext: {
517
+ theme: "light",
518
+ platform: "web",
519
+ },
520
+ });
521
+ break;
522
+ }
523
+
524
+ case "ui/message": {
525
+ // Add message to CopilotKit chat
526
+ const currentAgent = agentRef.current;
527
+
528
+ if (!currentAgent) {
529
+ console.warn(
530
+ "[MCPAppsRenderer] ui/message: No agent available",
531
+ );
532
+ sendResponse(msg.id, { isError: false });
533
+ break;
534
+ }
535
+
536
+ try {
537
+ const params = msg.params as {
538
+ role?: string;
539
+ content?: Array<{ type: string; text?: string }>;
540
+ };
541
+
542
+ // Extract text content from the message
543
+ const textContent =
544
+ params.content
545
+ ?.filter((c) => c.type === "text" && c.text)
546
+ .map((c) => c.text)
547
+ .join("\n") || "";
548
+
549
+ if (textContent) {
550
+ currentAgent.addMessage({
551
+ id: crypto.randomUUID(),
552
+ role: (params.role as "user" | "assistant") || "user",
553
+ content: textContent,
554
+ });
555
+ }
556
+ sendResponse(msg.id, { isError: false });
557
+ } catch (err) {
558
+ console.error("[MCPAppsRenderer] ui/message error:", err);
559
+ sendResponse(msg.id, { isError: true });
560
+ }
561
+ break;
562
+ }
563
+
564
+ case "ui/open-link": {
565
+ // Open URL in new tab
566
+ const url = msg.params?.url as string | undefined;
567
+ if (url) {
568
+ window.open(url, "_blank", "noopener,noreferrer");
569
+ sendResponse(msg.id, { isError: false });
570
+ } else {
571
+ sendErrorResponse(msg.id, -32602, "Missing url parameter");
572
+ }
573
+ break;
574
+ }
575
+
576
+ case "tools/call": {
577
+ // Proxy tool call to MCP server via agent.runAgent()
578
+ const { serverHash, serverId } = contentRef.current;
579
+ const currentAgent = agentRef.current;
580
+
581
+ if (!serverHash) {
582
+ sendErrorResponse(
583
+ msg.id,
584
+ -32603,
585
+ "No server hash available for proxying",
586
+ );
587
+ break;
588
+ }
589
+
590
+ if (!currentAgent) {
591
+ sendErrorResponse(
592
+ msg.id,
593
+ -32603,
594
+ "No agent available for proxying",
595
+ );
596
+ break;
597
+ }
598
+
599
+ try {
600
+ // Use queue to wait for agent to be idle and serialize requests
601
+ const runResult = await mcpAppsRequestQueue.enqueue(
602
+ currentAgent,
603
+ () =>
604
+ currentAgent.runAgent({
605
+ forwardedProps: {
606
+ __proxiedMCPRequest: {
607
+ serverHash,
608
+ serverId, // optional, takes precedence if provided
609
+ method: "tools/call",
610
+ params: msg.params,
611
+ },
612
+ },
613
+ }),
614
+ );
615
+
616
+ // The result from runAgent contains the MCP response
617
+ sendResponse(msg.id, runResult.result || {});
618
+ } catch (err) {
619
+ console.error("[MCPAppsRenderer] tools/call error:", err);
620
+ sendErrorResponse(msg.id, -32603, String(err));
621
+ }
622
+ break;
623
+ }
624
+
625
+ default:
626
+ sendErrorResponse(
627
+ msg.id,
628
+ -32601,
629
+ `Method not found: ${msg.method}`,
630
+ );
631
+ }
632
+ }
633
+
634
+ // Handle notifications (no response needed)
635
+ if (isNotification(msg)) {
636
+ switch (msg.method) {
637
+ case "ui/notifications/initialized": {
638
+ console.log("[MCPAppsRenderer] Inner iframe initialized");
639
+ if (mounted) {
640
+ setIframeReady(true);
641
+ }
642
+ break;
643
+ }
644
+
645
+ case "ui/notifications/size-changed": {
646
+ const { width, height } = msg.params || {};
647
+ console.log("[MCPAppsRenderer] Size change:", {
648
+ width,
649
+ height,
650
+ });
651
+ if (mounted) {
652
+ setIframeSize({
653
+ width: typeof width === "number" ? width : undefined,
654
+ height: typeof height === "number" ? height : undefined,
655
+ });
656
+ }
657
+ break;
658
+ }
659
+
660
+ case "notifications/message": {
661
+ // Logging notification from the app
662
+ console.log("[MCPAppsRenderer] App log:", msg.params);
663
+ break;
664
+ }
665
+ }
666
+ }
667
+ };
668
+
669
+ window.addEventListener("message", messageHandler);
670
+
671
+ // Extract HTML content from fetched resource
672
+ let html: string;
673
+ if (fetchedResource.text) {
674
+ html = fetchedResource.text;
675
+ } else if (fetchedResource.blob) {
676
+ html = atob(fetchedResource.blob);
677
+ } else {
678
+ throw new Error("Resource has no text or blob content");
679
+ }
680
+
681
+ // Send the resource content to the sandbox proxy
682
+ sendNotification("ui/notifications/sandbox-resource-ready", { html });
683
+ } catch (err) {
684
+ console.error("[MCPAppsRenderer] Setup error:", err);
685
+ if (mounted) {
686
+ setError(err instanceof Error ? err : new Error(String(err)));
687
+ }
688
+ }
689
+ };
690
+
691
+ setup();
692
+
693
+ return () => {
694
+ mounted = false;
695
+ // Clean up initial listener if still active
696
+ if (initialListener) {
697
+ window.removeEventListener("message", initialListener);
698
+ initialListener = null;
699
+ }
700
+ if (messageHandler) {
701
+ window.removeEventListener("message", messageHandler);
702
+ }
703
+ // Remove the iframe we created (using tracked reference, not DOM query)
704
+ // This works even if containerRef.current is null during unmount
705
+ if (createdIframe) {
706
+ createdIframe.remove();
707
+ createdIframe = null;
708
+ }
709
+ iframeRef.current = null;
710
+ };
711
+ }, [
712
+ isLoading,
713
+ fetchedResource,
714
+ sendNotification,
715
+ sendResponse,
716
+ sendErrorResponse,
717
+ ]);
718
+
719
+ // Effect 2: Update iframe size when it changes
720
+ useEffect(() => {
721
+ if (iframeRef.current) {
722
+ if (iframeSize.width !== undefined) {
723
+ // Use minWidth with min() to allow expansion but cap at 100%
724
+ iframeRef.current.style.minWidth = `min(${iframeSize.width}px, 100%)`;
725
+ iframeRef.current.style.width = "100%";
726
+ }
727
+ if (iframeSize.height !== undefined) {
728
+ iframeRef.current.style.height = `${iframeSize.height}px`;
729
+ }
730
+ }
731
+ }, [iframeSize]);
732
+
733
+ // Effect 3: Send tool input when iframe ready
734
+ useEffect(() => {
735
+ if (iframeReady && content.toolInput) {
736
+ console.log("[MCPAppsRenderer] Sending tool input:", content.toolInput);
737
+ sendNotification("ui/notifications/tool-input", {
738
+ arguments: content.toolInput,
739
+ });
740
+ }
741
+ }, [iframeReady, content.toolInput, sendNotification]);
742
+
743
+ // Effect 4: Send tool result when iframe ready
744
+ useEffect(() => {
745
+ if (iframeReady && content.result) {
746
+ console.log("[MCPAppsRenderer] Sending tool result:", content.result);
747
+ sendNotification("ui/notifications/tool-result", content.result);
748
+ }
749
+ }, [iframeReady, content.result, sendNotification]);
750
+
751
+ // Determine border styling based on prefersBorder metadata from fetched resource
752
+ // true = show border/background, false = none, undefined = host decides (we default to none)
753
+ const prefersBorder = fetchedResource?._meta?.ui?.prefersBorder;
754
+ const borderStyle =
755
+ prefersBorder === true
756
+ ? {
757
+ borderRadius: "8px",
758
+ backgroundColor: "#f9f9f9",
759
+ border: "1px solid #e0e0e0",
760
+ }
761
+ : {};
762
+
763
+ return (
764
+ <div
765
+ ref={containerRef}
766
+ style={{
767
+ width: "100%",
768
+ height: iframeSize.height ? `${iframeSize.height}px` : "auto",
769
+ minHeight: "100px",
770
+ overflow: "hidden",
771
+ position: "relative",
772
+ ...borderStyle,
773
+ }}
774
+ >
775
+ {isLoading && (
776
+ <div style={{ padding: "1rem", color: "#666" }}>Loading...</div>
777
+ )}
778
+ {error && (
779
+ <div style={{ color: "red", padding: "1rem" }}>
780
+ Error: {error.message}
781
+ </div>
782
+ )}
783
+ </div>
784
+ );
785
+ };