@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,1011 @@
1
+ /**
2
+ * End-to-end tests for MCP Apps Activity Renderer
3
+ *
4
+ * Tests the complete flow of rendering MCP Apps UI:
5
+ * 1. Activity snapshot received with resourceUri
6
+ * 2. Resource fetched via proxied MCP request
7
+ * 3. Sandboxed iframe created and communicates via JSON-RPC
8
+ * 4. Tool calls proxied back through the agent
9
+ */
10
+ import { fireEvent, screen, waitFor, act } from "@testing-library/react";
11
+ import { vi } from "vitest";
12
+ import {
13
+ activitySnapshotEvent,
14
+ renderWithCopilotKit,
15
+ runFinishedEvent,
16
+ runStartedEvent,
17
+ testId,
18
+ } from "../../../__tests__/utils/test-helpers";
19
+ import {
20
+ MCPAppsActivityType,
21
+ MCPAppsActivityContentSchema,
22
+ } from "../../../components/MCPAppsActivityRenderer";
23
+ import { ReactActivityMessageRenderer } from "../../../types";
24
+ import {
25
+ AbstractAgent,
26
+ RunAgentInput,
27
+ RunAgentResult,
28
+ BaseEvent,
29
+ EventType,
30
+ } from "@ag-ui/client";
31
+ import { Observable, Subject } from "rxjs";
32
+
33
+ /**
34
+ * Mock agent that intercepts runAgent calls for proxied MCP requests
35
+ * while preserving normal streaming behavior for regular runs.
36
+ */
37
+ class MockMCPProxyAgent extends AbstractAgent {
38
+ private subject = new Subject<BaseEvent>();
39
+
40
+ // Track runAgent calls for verification
41
+ public runAgentCalls: Array<{ input: Partial<RunAgentInput> }> = [];
42
+
43
+ // Configurable responses for proxied MCP requests
44
+ private runAgentResponses: Map<string, unknown> = new Map();
45
+
46
+ /**
47
+ * Set the response for a specific MCP method
48
+ */
49
+ setRunAgentResponse(method: string, response: unknown) {
50
+ this.runAgentResponses.set(method, response);
51
+ }
52
+
53
+ /**
54
+ * Emit a single agent event
55
+ */
56
+ emit(event: BaseEvent) {
57
+ if (event.type === EventType.RUN_STARTED) {
58
+ this.isRunning = true;
59
+ } else if (
60
+ event.type === EventType.RUN_FINISHED ||
61
+ event.type === EventType.RUN_ERROR
62
+ ) {
63
+ this.isRunning = false;
64
+ }
65
+ act(() => {
66
+ this.subject.next(event);
67
+ });
68
+ }
69
+
70
+ /**
71
+ * Complete the agent stream
72
+ */
73
+ complete() {
74
+ this.isRunning = false;
75
+ act(() => {
76
+ this.subject.complete();
77
+ });
78
+ }
79
+
80
+ clone(): MockMCPProxyAgent {
81
+ return this;
82
+ }
83
+
84
+ async detachActiveRun(): Promise<void> {}
85
+
86
+ run(_input: RunAgentInput): Observable<BaseEvent> {
87
+ return this.subject.asObservable();
88
+ }
89
+
90
+ /**
91
+ * Override runAgent to intercept proxied MCP requests only.
92
+ * For normal message flows, delegate to the parent class.
93
+ */
94
+ async runAgent(input?: Partial<RunAgentInput>): Promise<RunAgentResult> {
95
+ const proxiedRequest = input?.forwardedProps?.__proxiedMCPRequest as
96
+ | {
97
+ serverHash?: string;
98
+ serverId?: string;
99
+ method: string;
100
+ params?: Record<string, unknown>;
101
+ }
102
+ | undefined;
103
+
104
+ // Only intercept proxied MCP requests
105
+ if (proxiedRequest) {
106
+ if (input) {
107
+ this.runAgentCalls.push({ input });
108
+ }
109
+
110
+ const method = proxiedRequest.method;
111
+ const response = this.runAgentResponses.get(method);
112
+
113
+ if (response !== undefined) {
114
+ return { result: response, newMessages: [] };
115
+ }
116
+
117
+ // Default responses
118
+ if (method === "resources/read") {
119
+ return {
120
+ result: {
121
+ contents: [
122
+ {
123
+ uri: proxiedRequest.params?.uri,
124
+ mimeType: "text/html",
125
+ text: "<html><body>Test content</body></html>",
126
+ },
127
+ ],
128
+ },
129
+ newMessages: [],
130
+ };
131
+ }
132
+
133
+ if (method === "tools/call") {
134
+ return {
135
+ result: {
136
+ content: [{ type: "text", text: "Tool call result" }],
137
+ isError: false,
138
+ },
139
+ newMessages: [],
140
+ };
141
+ }
142
+
143
+ return { result: {}, newMessages: [] };
144
+ }
145
+
146
+ // For normal runs (user messages), use the parent's runAgent which
147
+ // properly subscribes to run() and processes streaming events
148
+ return super.runAgent(input);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Helper to create MCP Apps activity content matching the 0.0.2 schema
154
+ */
155
+ function mcpAppsActivityContent(overrides: {
156
+ resourceUri?: string;
157
+ serverHash?: string;
158
+ serverId?: string;
159
+ toolInput?: Record<string, unknown>;
160
+ result?: {
161
+ content?: unknown[];
162
+ structuredContent?: unknown;
163
+ isError?: boolean;
164
+ };
165
+ }) {
166
+ return {
167
+ resourceUri: overrides.resourceUri ?? "ui://test-server/test-resource",
168
+ serverHash: overrides.serverHash ?? "abc123hash",
169
+ serverId: overrides.serverId,
170
+ toolInput: overrides.toolInput ?? {},
171
+ result: overrides.result ?? {
172
+ content: [{ type: "text", text: "Tool output" }],
173
+ isError: false,
174
+ },
175
+ };
176
+ }
177
+
178
+ describe("MCP Apps Activity Renderer E2E", () => {
179
+ beforeEach(() => {
180
+ // Reset any global state
181
+ vi.clearAllMocks();
182
+ });
183
+
184
+ describe("Resource Fetching", () => {
185
+ it("fetches resource content via proxied MCP request on mount", async () => {
186
+ const agent = new MockMCPProxyAgent();
187
+ const agentId = "mcp-test-agent";
188
+ agent.agentId = agentId;
189
+
190
+ // Set up response for resources/read
191
+ agent.setRunAgentResponse("resources/read", {
192
+ contents: [
193
+ {
194
+ uri: "ui://test-server/dashboard",
195
+ mimeType: "text/html",
196
+ text: "<html><body>Dashboard content</body></html>",
197
+ },
198
+ ],
199
+ });
200
+
201
+ renderWithCopilotKit({
202
+ agents: { [agentId]: agent },
203
+ agentId,
204
+ });
205
+
206
+ const input = await screen.findByRole("textbox");
207
+ fireEvent.change(input, { target: { value: "Show dashboard" } });
208
+ fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
209
+
210
+ await waitFor(() => {
211
+ expect(screen.getByText("Show dashboard")).toBeDefined();
212
+ });
213
+
214
+ const activityMessageId = testId("mcp-activity");
215
+ agent.emit(runStartedEvent());
216
+ agent.emit(
217
+ activitySnapshotEvent({
218
+ messageId: activityMessageId,
219
+ activityType: MCPAppsActivityType,
220
+ content: mcpAppsActivityContent({
221
+ resourceUri: "ui://test-server/dashboard",
222
+ serverHash: "dashboard-hash-123",
223
+ }),
224
+ }),
225
+ );
226
+ agent.emit(runFinishedEvent());
227
+
228
+ // Wait for the activity renderer to mount and show loading
229
+ await waitFor(
230
+ () => {
231
+ expect(screen.getByText("Loading...")).toBeDefined();
232
+ },
233
+ { timeout: 2000 },
234
+ );
235
+
236
+ // Wait for resource fetch to be called
237
+ await waitFor(
238
+ () => {
239
+ expect(agent.runAgentCalls.length).toBeGreaterThan(0);
240
+ },
241
+ { timeout: 2000 },
242
+ );
243
+
244
+ // Verify the proxied MCP request was made correctly
245
+ const resourceCall = agent.runAgentCalls.find(
246
+ (call) =>
247
+ call.input.forwardedProps?.__proxiedMCPRequest?.method ===
248
+ "resources/read",
249
+ );
250
+ expect(resourceCall).toBeDefined();
251
+ expect(
252
+ resourceCall?.input.forwardedProps?.__proxiedMCPRequest,
253
+ ).toMatchObject({
254
+ serverHash: "dashboard-hash-123",
255
+ method: "resources/read",
256
+ params: { uri: "ui://test-server/dashboard" },
257
+ });
258
+ });
259
+
260
+ it("uses serverId when provided (takes precedence over serverHash)", async () => {
261
+ const agent = new MockMCPProxyAgent();
262
+ const agentId = "mcp-test-agent";
263
+ agent.agentId = agentId;
264
+
265
+ agent.setRunAgentResponse("resources/read", {
266
+ contents: [
267
+ {
268
+ uri: "ui://my-app/settings",
269
+ mimeType: "text/html",
270
+ text: "<html><body>Settings</body></html>",
271
+ },
272
+ ],
273
+ });
274
+
275
+ renderWithCopilotKit({
276
+ agents: { [agentId]: agent },
277
+ agentId,
278
+ });
279
+
280
+ const input = await screen.findByRole("textbox");
281
+ fireEvent.change(input, { target: { value: "Show settings" } });
282
+ fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
283
+
284
+ await waitFor(() => {
285
+ expect(screen.getByText("Show settings")).toBeDefined();
286
+ });
287
+
288
+ agent.emit(runStartedEvent());
289
+ agent.emit(
290
+ activitySnapshotEvent({
291
+ messageId: testId("mcp-activity"),
292
+ activityType: MCPAppsActivityType,
293
+ content: mcpAppsActivityContent({
294
+ resourceUri: "ui://my-app/settings",
295
+ serverHash: "fallback-hash",
296
+ serverId: "my-app-stable-id", // Should take precedence
297
+ }),
298
+ }),
299
+ );
300
+ agent.emit(runFinishedEvent());
301
+
302
+ await waitFor(() => {
303
+ const resourceCall = agent.runAgentCalls.find(
304
+ (call) =>
305
+ call.input.forwardedProps?.__proxiedMCPRequest?.method ===
306
+ "resources/read",
307
+ );
308
+ expect(resourceCall).toBeDefined();
309
+ expect(
310
+ resourceCall?.input.forwardedProps?.__proxiedMCPRequest?.serverId,
311
+ ).toBe("my-app-stable-id");
312
+ expect(
313
+ resourceCall?.input.forwardedProps?.__proxiedMCPRequest?.serverHash,
314
+ ).toBe("fallback-hash");
315
+ });
316
+ });
317
+
318
+ it("shows loading state while fetching resource", async () => {
319
+ const agent = new MockMCPProxyAgent();
320
+ const agentId = "mcp-test-agent";
321
+ agent.agentId = agentId;
322
+
323
+ // Create a promise that we can control
324
+ let resolveResource: (value: unknown) => void;
325
+ const resourcePromise = new Promise((resolve) => {
326
+ resolveResource = resolve;
327
+ });
328
+
329
+ // Override runAgent to use our controlled promise
330
+ const originalRunAgent = agent.runAgent.bind(agent);
331
+ agent.runAgent = async (
332
+ input?: Partial<RunAgentInput>,
333
+ ): Promise<RunAgentResult> => {
334
+ const proxiedRequest = input?.forwardedProps?.__proxiedMCPRequest as
335
+ | { method: string }
336
+ | undefined;
337
+ if (proxiedRequest?.method === "resources/read") {
338
+ await resourcePromise;
339
+ return originalRunAgent(input);
340
+ }
341
+ return originalRunAgent(input);
342
+ };
343
+
344
+ renderWithCopilotKit({
345
+ agents: { [agentId]: agent },
346
+ agentId,
347
+ });
348
+
349
+ const input = await screen.findByRole("textbox");
350
+ fireEvent.change(input, { target: { value: "Load app" } });
351
+ fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
352
+
353
+ await waitFor(() => {
354
+ expect(screen.getByText("Load app")).toBeDefined();
355
+ });
356
+
357
+ agent.emit(runStartedEvent());
358
+ agent.emit(
359
+ activitySnapshotEvent({
360
+ messageId: testId("mcp-activity"),
361
+ activityType: MCPAppsActivityType,
362
+ content: mcpAppsActivityContent({
363
+ resourceUri: "ui://test/app",
364
+ serverHash: "test-hash",
365
+ }),
366
+ }),
367
+ );
368
+ agent.emit(runFinishedEvent());
369
+
370
+ // Should show loading state
371
+ await waitFor(() => {
372
+ expect(screen.getByText("Loading...")).toBeDefined();
373
+ });
374
+
375
+ // Resolve the resource fetch
376
+ act(() => {
377
+ resolveResource!(true);
378
+ });
379
+
380
+ // Loading should eventually disappear (iframe created)
381
+ await waitFor(
382
+ () => {
383
+ expect(screen.queryByText("Loading...")).toBeNull();
384
+ },
385
+ { timeout: 3000 },
386
+ );
387
+ });
388
+
389
+ it("shows error state when resource fetch fails", async () => {
390
+ const agent = new MockMCPProxyAgent();
391
+ const agentId = "mcp-test-agent";
392
+ agent.agentId = agentId;
393
+
394
+ // Make proxied MCP requests throw an error
395
+ const originalRunAgent = agent.runAgent.bind(agent);
396
+ agent.runAgent = async (
397
+ input?: Partial<RunAgentInput>,
398
+ ): Promise<RunAgentResult> => {
399
+ const proxiedRequest = input?.forwardedProps?.__proxiedMCPRequest as
400
+ | { method: string }
401
+ | undefined;
402
+ if (proxiedRequest) {
403
+ throw new Error("Network error: Failed to fetch resource");
404
+ }
405
+ return originalRunAgent(input);
406
+ };
407
+
408
+ renderWithCopilotKit({
409
+ agents: { [agentId]: agent },
410
+ agentId,
411
+ });
412
+
413
+ const input = await screen.findByRole("textbox");
414
+ fireEvent.change(input, { target: { value: "Fetch broken" } });
415
+ fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
416
+
417
+ await waitFor(() => {
418
+ expect(screen.getByText("Fetch broken")).toBeDefined();
419
+ });
420
+
421
+ agent.emit(runStartedEvent());
422
+ agent.emit(
423
+ activitySnapshotEvent({
424
+ messageId: testId("mcp-activity"),
425
+ activityType: MCPAppsActivityType,
426
+ content: mcpAppsActivityContent({
427
+ resourceUri: "ui://broken/resource",
428
+ serverHash: "broken-hash",
429
+ }),
430
+ }),
431
+ );
432
+ agent.emit(runFinishedEvent());
433
+
434
+ // Should show error state
435
+ await waitFor(() => {
436
+ expect(
437
+ screen.getByText(/Error:.*Failed to fetch resource/i),
438
+ ).toBeDefined();
439
+ });
440
+ });
441
+
442
+ it("handles resource with no content gracefully", async () => {
443
+ const agent = new MockMCPProxyAgent();
444
+ const agentId = "mcp-test-agent";
445
+ agent.agentId = agentId;
446
+
447
+ // Return empty contents
448
+ agent.setRunAgentResponse("resources/read", {
449
+ contents: [],
450
+ });
451
+
452
+ renderWithCopilotKit({
453
+ agents: { [agentId]: agent },
454
+ agentId,
455
+ });
456
+
457
+ const input = await screen.findByRole("textbox");
458
+ fireEvent.change(input, { target: { value: "Empty resource" } });
459
+ fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
460
+
461
+ await waitFor(() => {
462
+ expect(screen.getByText("Empty resource")).toBeDefined();
463
+ });
464
+
465
+ agent.emit(runStartedEvent());
466
+ agent.emit(
467
+ activitySnapshotEvent({
468
+ messageId: testId("mcp-activity"),
469
+ activityType: MCPAppsActivityType,
470
+ content: mcpAppsActivityContent({
471
+ resourceUri: "ui://empty/resource",
472
+ serverHash: "empty-hash",
473
+ }),
474
+ }),
475
+ );
476
+ agent.emit(runFinishedEvent());
477
+
478
+ // Should show error about no content
479
+ await waitFor(() => {
480
+ expect(screen.getByText(/Error:.*No resource content/i)).toBeDefined();
481
+ });
482
+ });
483
+ });
484
+
485
+ describe("Schema Validation", () => {
486
+ it("validates activity content with the correct schema", () => {
487
+ // Valid content
488
+ const validContent = {
489
+ resourceUri: "ui://server/resource",
490
+ serverHash: "hash123",
491
+ result: {
492
+ content: [{ type: "text", text: "output" }],
493
+ isError: false,
494
+ },
495
+ };
496
+
497
+ const validResult = MCPAppsActivityContentSchema.safeParse(validContent);
498
+ expect(validResult.success).toBe(true);
499
+
500
+ // With optional serverId
501
+ const withServerId = {
502
+ ...validContent,
503
+ serverId: "stable-server-id",
504
+ };
505
+ const serverIdResult =
506
+ MCPAppsActivityContentSchema.safeParse(withServerId);
507
+ expect(serverIdResult.success).toBe(true);
508
+
509
+ // With toolInput
510
+ const withToolInput = {
511
+ ...validContent,
512
+ toolInput: { param1: "value1", param2: 42 },
513
+ };
514
+ const toolInputResult =
515
+ MCPAppsActivityContentSchema.safeParse(withToolInput);
516
+ expect(toolInputResult.success).toBe(true);
517
+ });
518
+
519
+ it("rejects invalid activity content", () => {
520
+ // Missing required fields
521
+ const missingResourceUri = {
522
+ serverHash: "hash123",
523
+ result: { isError: false },
524
+ };
525
+ expect(
526
+ MCPAppsActivityContentSchema.safeParse(missingResourceUri).success,
527
+ ).toBe(false);
528
+
529
+ const missingServerHash = {
530
+ resourceUri: "ui://server/resource",
531
+ result: { isError: false },
532
+ };
533
+ expect(
534
+ MCPAppsActivityContentSchema.safeParse(missingServerHash).success,
535
+ ).toBe(false);
536
+
537
+ const missingResult = {
538
+ resourceUri: "ui://server/resource",
539
+ serverHash: "hash123",
540
+ };
541
+ expect(
542
+ MCPAppsActivityContentSchema.safeParse(missingResult).success,
543
+ ).toBe(false);
544
+ });
545
+ });
546
+
547
+ describe("Activity Type Integration", () => {
548
+ it("built-in MCP Apps renderer is registered with correct activity type", async () => {
549
+ const agent = new MockMCPProxyAgent();
550
+ const agentId = "mcp-test-agent";
551
+ agent.agentId = agentId;
552
+
553
+ renderWithCopilotKit({
554
+ agents: { [agentId]: agent },
555
+ agentId,
556
+ // Don't pass any custom renderers - built-in should be used
557
+ });
558
+
559
+ const input = await screen.findByRole("textbox");
560
+ fireEvent.change(input, { target: { value: "Test MCP" } });
561
+ fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
562
+
563
+ await waitFor(() => {
564
+ expect(screen.getByText("Test MCP")).toBeDefined();
565
+ });
566
+
567
+ agent.emit(runStartedEvent());
568
+ agent.emit(
569
+ activitySnapshotEvent({
570
+ messageId: testId("mcp-activity"),
571
+ activityType: "mcp-apps", // Should match MCPAppsActivityType
572
+ content: mcpAppsActivityContent({
573
+ resourceUri: "ui://builtin/test",
574
+ serverHash: "builtin-hash",
575
+ }),
576
+ }),
577
+ );
578
+ agent.emit(runFinishedEvent());
579
+
580
+ // Should show loading (meaning the renderer was matched)
581
+ await waitFor(() => {
582
+ expect(screen.getByText("Loading...")).toBeDefined();
583
+ });
584
+ });
585
+
586
+ it("user-provided renderer takes precedence over built-in", async () => {
587
+ const agent = new MockMCPProxyAgent();
588
+ const agentId = "mcp-test-agent";
589
+ agent.agentId = agentId;
590
+
591
+ // Custom renderer that overrides the built-in
592
+ const customRenderer: ReactActivityMessageRenderer<unknown> = {
593
+ activityType: MCPAppsActivityType,
594
+ content: MCPAppsActivityContentSchema,
595
+ render: ({ content }) => (
596
+ <div data-testid="custom-mcp-renderer">
597
+ Custom MCP Renderer: {(content as any).resourceUri}
598
+ </div>
599
+ ),
600
+ };
601
+
602
+ renderWithCopilotKit({
603
+ agents: { [agentId]: agent },
604
+ agentId,
605
+ renderActivityMessages: [customRenderer],
606
+ });
607
+
608
+ const input = await screen.findByRole("textbox");
609
+ fireEvent.change(input, { target: { value: "Custom renderer" } });
610
+ fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
611
+
612
+ await waitFor(() => {
613
+ expect(screen.getByText("Custom renderer")).toBeDefined();
614
+ });
615
+
616
+ agent.emit(runStartedEvent());
617
+ agent.emit(
618
+ activitySnapshotEvent({
619
+ messageId: testId("mcp-activity"),
620
+ activityType: MCPAppsActivityType,
621
+ content: mcpAppsActivityContent({
622
+ resourceUri: "ui://custom/resource",
623
+ serverHash: "custom-hash",
624
+ }),
625
+ }),
626
+ );
627
+ agent.emit(runFinishedEvent());
628
+
629
+ // Should render custom component, not loading
630
+ await waitFor(() => {
631
+ expect(screen.getByTestId("custom-mcp-renderer")).toBeDefined();
632
+ expect(
633
+ screen.getByText(/Custom MCP Renderer:.*ui:\/\/custom\/resource/),
634
+ ).toBeDefined();
635
+ });
636
+ });
637
+ });
638
+
639
+ describe("Multiple Activity Messages", () => {
640
+ it("renders multiple MCP Apps activities independently", async () => {
641
+ const agent = new MockMCPProxyAgent();
642
+ const agentId = "mcp-test-agent";
643
+ agent.agentId = agentId;
644
+
645
+ // Set up different responses for different URIs
646
+ const originalRunAgent = agent.runAgent.bind(agent);
647
+ agent.runAgent = async (
648
+ input?: Partial<RunAgentInput>,
649
+ ): Promise<RunAgentResult> => {
650
+ const proxiedRequest = input?.forwardedProps?.__proxiedMCPRequest as {
651
+ method: string;
652
+ params?: { uri?: string };
653
+ };
654
+ if (proxiedRequest?.method === "resources/read") {
655
+ const uri = proxiedRequest.params?.uri;
656
+ if (uri === "ui://first/app") {
657
+ return {
658
+ result: {
659
+ contents: [
660
+ { uri, mimeType: "text/html", text: "<div>First App</div>" },
661
+ ],
662
+ },
663
+ newMessages: [],
664
+ };
665
+ }
666
+ if (uri === "ui://second/app") {
667
+ return {
668
+ result: {
669
+ contents: [
670
+ { uri, mimeType: "text/html", text: "<div>Second App</div>" },
671
+ ],
672
+ },
673
+ newMessages: [],
674
+ };
675
+ }
676
+ }
677
+ // For non-proxied requests, use original behavior
678
+ return originalRunAgent(input);
679
+ };
680
+
681
+ renderWithCopilotKit({
682
+ agents: { [agentId]: agent },
683
+ agentId,
684
+ });
685
+
686
+ const input = await screen.findByRole("textbox");
687
+ fireEvent.change(input, { target: { value: "Multiple apps" } });
688
+ fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
689
+
690
+ await waitFor(() => {
691
+ expect(screen.getByText("Multiple apps")).toBeDefined();
692
+ });
693
+
694
+ agent.emit(runStartedEvent());
695
+
696
+ // Emit two activity messages
697
+ agent.emit(
698
+ activitySnapshotEvent({
699
+ messageId: testId("mcp-first"),
700
+ activityType: MCPAppsActivityType,
701
+ content: mcpAppsActivityContent({
702
+ resourceUri: "ui://first/app",
703
+ serverHash: "first-hash",
704
+ }),
705
+ }),
706
+ );
707
+
708
+ agent.emit(
709
+ activitySnapshotEvent({
710
+ messageId: testId("mcp-second"),
711
+ activityType: MCPAppsActivityType,
712
+ content: mcpAppsActivityContent({
713
+ resourceUri: "ui://second/app",
714
+ serverHash: "second-hash",
715
+ }),
716
+ }),
717
+ );
718
+
719
+ agent.emit(runFinishedEvent());
720
+
721
+ // Both activities should trigger resource fetches and create iframes.
722
+ // Due to async timing, the loading states might clear quickly,
723
+ // so we verify both iframes are eventually created.
724
+ await waitFor(
725
+ () => {
726
+ const iframes = document.querySelectorAll("iframe[srcdoc]");
727
+ expect(iframes.length).toBe(2);
728
+ },
729
+ { timeout: 2000 },
730
+ );
731
+ });
732
+ });
733
+
734
+ describe("Content Types", () => {
735
+ it("handles text content from resource", async () => {
736
+ const agent = new MockMCPProxyAgent();
737
+ const agentId = "mcp-test-agent";
738
+ agent.agentId = agentId;
739
+
740
+ agent.setRunAgentResponse("resources/read", {
741
+ contents: [
742
+ {
743
+ uri: "ui://test/text",
744
+ mimeType: "text/html",
745
+ text: "<html><body><h1>Text Content</h1></body></html>",
746
+ },
747
+ ],
748
+ });
749
+
750
+ renderWithCopilotKit({
751
+ agents: { [agentId]: agent },
752
+ agentId,
753
+ });
754
+
755
+ const input = await screen.findByRole("textbox");
756
+ fireEvent.change(input, { target: { value: "Text content" } });
757
+ fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
758
+
759
+ await waitFor(() => {
760
+ expect(screen.getByText("Text content")).toBeDefined();
761
+ });
762
+
763
+ agent.emit(runStartedEvent());
764
+ agent.emit(
765
+ activitySnapshotEvent({
766
+ messageId: testId("mcp-activity"),
767
+ activityType: MCPAppsActivityType,
768
+ content: mcpAppsActivityContent({
769
+ resourceUri: "ui://test/text",
770
+ serverHash: "text-hash",
771
+ }),
772
+ }),
773
+ );
774
+ agent.emit(runFinishedEvent());
775
+
776
+ // Should transition from loading to rendered
777
+ await waitFor(() => {
778
+ expect(screen.getByText("Loading...")).toBeDefined();
779
+ });
780
+ });
781
+
782
+ it("handles blob (base64) content from resource", async () => {
783
+ const agent = new MockMCPProxyAgent();
784
+ const agentId = "mcp-test-agent";
785
+ agent.agentId = agentId;
786
+
787
+ // Base64 encoded "<html><body>Blob Content</body></html>"
788
+ const base64Html = btoa("<html><body>Blob Content</body></html>");
789
+
790
+ agent.setRunAgentResponse("resources/read", {
791
+ contents: [
792
+ {
793
+ uri: "ui://test/blob",
794
+ mimeType: "text/html",
795
+ blob: base64Html,
796
+ },
797
+ ],
798
+ });
799
+
800
+ renderWithCopilotKit({
801
+ agents: { [agentId]: agent },
802
+ agentId,
803
+ });
804
+
805
+ const input = await screen.findByRole("textbox");
806
+ fireEvent.change(input, { target: { value: "Blob content" } });
807
+ fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
808
+
809
+ await waitFor(() => {
810
+ expect(screen.getByText("Blob content")).toBeDefined();
811
+ });
812
+
813
+ agent.emit(runStartedEvent());
814
+ agent.emit(
815
+ activitySnapshotEvent({
816
+ messageId: testId("mcp-activity"),
817
+ activityType: MCPAppsActivityType,
818
+ content: mcpAppsActivityContent({
819
+ resourceUri: "ui://test/blob",
820
+ serverHash: "blob-hash",
821
+ }),
822
+ }),
823
+ );
824
+ agent.emit(runFinishedEvent());
825
+
826
+ // Should show loading initially
827
+ await waitFor(() => {
828
+ expect(screen.getByText("Loading...")).toBeDefined();
829
+ });
830
+ });
831
+
832
+ it("handles resource with no text or blob - iframe created but stuck waiting for sandbox", async () => {
833
+ // NOTE: In jsdom, the sandbox iframe (using srcdoc) can't fully execute, so the
834
+ // component will create an iframe and wait for the sandbox proxy to be ready.
835
+ // The actual error for missing text/blob happens inside the sandbox communication
836
+ // flow which can't complete in jsdom. This test verifies that:
837
+ // 1. The component fetches the resource successfully
838
+ // 2. The iframe is created (showing the component progressed past loading)
839
+
840
+ const agent = new MockMCPProxyAgent();
841
+ const agentId = "mcp-test-agent";
842
+ agent.agentId = agentId;
843
+
844
+ // Resource with neither text nor blob
845
+ agent.setRunAgentResponse("resources/read", {
846
+ contents: [
847
+ {
848
+ uri: "ui://test/empty",
849
+ mimeType: "text/html",
850
+ // No text or blob field - in real environment this would cause an error
851
+ // after sandbox proxy is ready, but in jsdom the proxy never responds
852
+ },
853
+ ],
854
+ });
855
+
856
+ renderWithCopilotKit({
857
+ agents: { [agentId]: agent },
858
+ agentId,
859
+ });
860
+
861
+ const input = await screen.findByRole("textbox");
862
+ fireEvent.change(input, { target: { value: "No content" } });
863
+ fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
864
+
865
+ await waitFor(() => {
866
+ expect(screen.getByText("No content")).toBeDefined();
867
+ });
868
+
869
+ agent.emit(runStartedEvent());
870
+ agent.emit(
871
+ activitySnapshotEvent({
872
+ messageId: testId("mcp-activity"),
873
+ activityType: MCPAppsActivityType,
874
+ content: mcpAppsActivityContent({
875
+ resourceUri: "ui://test/empty",
876
+ serverHash: "empty-hash",
877
+ }),
878
+ }),
879
+ );
880
+ agent.emit(runFinishedEvent());
881
+
882
+ // Verify the iframe is created (component progressed past loading)
883
+ // In jsdom, the sandbox proxy never responds, so the error for missing text/blob
884
+ // is never reached. This is a limitation of jsdom testing.
885
+ await waitFor(
886
+ () => {
887
+ const iframe = document.querySelector("iframe[srcdoc]");
888
+ expect(iframe).not.toBeNull();
889
+ },
890
+ { timeout: 2000 },
891
+ );
892
+ });
893
+ });
894
+
895
+ describe("Metadata Handling", () => {
896
+ it("applies border styling when prefersBorder is true", async () => {
897
+ const agent = new MockMCPProxyAgent();
898
+ const agentId = "mcp-test-agent";
899
+ agent.agentId = agentId;
900
+
901
+ agent.setRunAgentResponse("resources/read", {
902
+ contents: [
903
+ {
904
+ uri: "ui://test/bordered",
905
+ mimeType: "text/html",
906
+ text: "<html><body>Bordered Content</body></html>",
907
+ _meta: {
908
+ ui: {
909
+ prefersBorder: true,
910
+ },
911
+ },
912
+ },
913
+ ],
914
+ });
915
+
916
+ renderWithCopilotKit({
917
+ agents: { [agentId]: agent },
918
+ agentId,
919
+ });
920
+
921
+ const input = await screen.findByRole("textbox");
922
+ fireEvent.change(input, { target: { value: "Bordered app" } });
923
+ fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
924
+
925
+ await waitFor(() => {
926
+ expect(screen.getByText("Bordered app")).toBeDefined();
927
+ });
928
+
929
+ agent.emit(runStartedEvent());
930
+ agent.emit(
931
+ activitySnapshotEvent({
932
+ messageId: testId("mcp-activity"),
933
+ activityType: MCPAppsActivityType,
934
+ content: mcpAppsActivityContent({
935
+ resourceUri: "ui://test/bordered",
936
+ serverHash: "bordered-hash",
937
+ }),
938
+ }),
939
+ );
940
+ agent.emit(runFinishedEvent());
941
+
942
+ // Wait for resource to be fetched and iframe to be created
943
+ await waitFor(
944
+ () => {
945
+ // Loading should disappear
946
+ expect(screen.queryByText("Loading...")).toBeNull();
947
+ // Iframe should be created
948
+ const iframe = document.querySelector("iframe[srcdoc]");
949
+ expect(iframe).not.toBeNull();
950
+ },
951
+ { timeout: 3000 },
952
+ );
953
+
954
+ // Note: Border styling is applied via inline styles based on prefersBorder metadata.
955
+ // In jsdom, verifying inline styles is not reliable, but we've verified the component
956
+ // renders successfully with the metadata that includes prefersBorder: true.
957
+ });
958
+
959
+ it("does not apply border styling when prefersBorder is false", async () => {
960
+ const agent = new MockMCPProxyAgent();
961
+ const agentId = "mcp-test-agent";
962
+ agent.agentId = agentId;
963
+
964
+ agent.setRunAgentResponse("resources/read", {
965
+ contents: [
966
+ {
967
+ uri: "ui://test/borderless",
968
+ mimeType: "text/html",
969
+ text: "<html><body>Borderless Content</body></html>",
970
+ _meta: {
971
+ ui: {
972
+ prefersBorder: false,
973
+ },
974
+ },
975
+ },
976
+ ],
977
+ });
978
+
979
+ renderWithCopilotKit({
980
+ agents: { [agentId]: agent },
981
+ agentId,
982
+ });
983
+
984
+ const input = await screen.findByRole("textbox");
985
+ fireEvent.change(input, { target: { value: "Borderless app" } });
986
+ fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
987
+
988
+ await waitFor(() => {
989
+ expect(screen.getByText("Borderless app")).toBeDefined();
990
+ });
991
+
992
+ agent.emit(runStartedEvent());
993
+ agent.emit(
994
+ activitySnapshotEvent({
995
+ messageId: testId("mcp-activity"),
996
+ activityType: MCPAppsActivityType,
997
+ content: mcpAppsActivityContent({
998
+ resourceUri: "ui://test/borderless",
999
+ serverHash: "borderless-hash",
1000
+ }),
1001
+ }),
1002
+ );
1003
+ agent.emit(runFinishedEvent());
1004
+
1005
+ // Verify component renders without error
1006
+ await waitFor(() => {
1007
+ expect(screen.getByText("Loading...")).toBeDefined();
1008
+ });
1009
+ });
1010
+ });
1011
+ });