@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.
- package/CHANGELOG.md +127 -116
- package/dist/copilotkit-B3Mb1yVE.cjs +7975 -0
- package/dist/copilotkit-B3Mb1yVE.cjs.map +1 -0
- package/dist/copilotkit-DBzgOMby.d.cts +2182 -0
- package/dist/copilotkit-DBzgOMby.d.cts.map +1 -0
- package/dist/copilotkit-DNYSFuz5.mjs +7562 -0
- package/dist/copilotkit-DNYSFuz5.mjs.map +1 -0
- package/dist/copilotkit-Dy5w3qEV.d.mts +2182 -0
- package/dist/copilotkit-Dy5w3qEV.d.mts.map +1 -0
- package/dist/index.cjs +27 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4 -5
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +1941 -35
- package/dist/index.umd.js.map +1 -1
- package/dist/v2/index.cjs +77 -7
- package/dist/v2/index.css +1 -2
- package/dist/v2/index.d.cts +6 -4
- package/dist/v2/index.d.mts +6 -4
- package/dist/v2/index.mjs +7 -4
- package/dist/v2/index.umd.js +5725 -24
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +37 -9
- package/scripts/scope-preflight.mjs +101 -0
- package/src/components/CopilotListeners.tsx +2 -6
- package/src/components/copilot-provider/copilot-messages.tsx +1 -1
- package/src/components/copilot-provider/copilotkit-props.tsx +1 -1
- package/src/components/copilot-provider/copilotkit.tsx +4 -4
- package/src/context/copilot-messages-context.tsx +1 -1
- package/src/hooks/__tests__/use-coagent-config.test.ts +2 -2
- package/src/hooks/__tests__/use-coagent-state-render.e2e.test.tsx +2 -2
- package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +3 -7
- package/src/hooks/__tests__/use-frontend-tool-available.test.tsx +1 -1
- package/src/hooks/__tests__/use-frontend-tool-remount.e2e.test.tsx +4 -4
- package/src/hooks/use-agent-nodename.ts +1 -1
- package/src/hooks/use-coagent-state-render-bridge.tsx +1 -4
- package/src/hooks/use-coagent.ts +1 -1
- package/src/hooks/use-configure-chat-suggestions.tsx +2 -2
- package/src/hooks/use-copilot-chat-suggestions.tsx +2 -2
- package/src/hooks/use-copilot-chat_internal.ts +2 -2
- package/src/hooks/use-copilot-readable.ts +1 -1
- package/src/hooks/use-frontend-tool.ts +2 -2
- package/src/hooks/use-human-in-the-loop.ts +2 -2
- package/src/hooks/use-langgraph-interrupt.ts +2 -5
- package/src/hooks/use-lazy-tool-renderer.tsx +1 -1
- package/src/hooks/use-render-tool-call.ts +1 -1
- package/src/lib/copilot-task.ts +1 -1
- package/src/setupTests.ts +18 -14
- package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +176 -0
- package/src/v2/__tests__/globalSetup.ts +14 -0
- package/src/v2/__tests__/setup.ts +93 -0
- package/src/v2/__tests__/utils/test-helpers.tsx +470 -0
- package/src/v2/a2ui/A2UIMessageRenderer.tsx +206 -0
- package/src/v2/components/CopilotKitInspector.tsx +50 -0
- package/src/v2/components/MCPAppsActivityRenderer.tsx +785 -0
- package/src/v2/components/WildcardToolCallRender.tsx +86 -0
- package/src/v2/components/__tests__/license-warning-banner.test.tsx +46 -0
- package/src/v2/components/chat/CopilotChat.tsx +431 -0
- package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +375 -0
- package/src/v2/components/chat/CopilotChatAudioRecorder.tsx +350 -0
- package/src/v2/components/chat/CopilotChatInput.tsx +1302 -0
- package/src/v2/components/chat/CopilotChatMessageView.tsx +556 -0
- package/src/v2/components/chat/CopilotChatReasoningMessage.tsx +252 -0
- package/src/v2/components/chat/CopilotChatSuggestionPill.tsx +59 -0
- package/src/v2/components/chat/CopilotChatSuggestionView.tsx +133 -0
- package/src/v2/components/chat/CopilotChatToggleButton.tsx +171 -0
- package/src/v2/components/chat/CopilotChatToolCallsView.tsx +40 -0
- package/src/v2/components/chat/CopilotChatUserMessage.tsx +388 -0
- package/src/v2/components/chat/CopilotChatView.tsx +598 -0
- package/src/v2/components/chat/CopilotModalHeader.tsx +129 -0
- package/src/v2/components/chat/CopilotPopup.tsx +81 -0
- package/src/v2/components/chat/CopilotPopupView.tsx +317 -0
- package/src/v2/components/chat/CopilotSidebar.tsx +76 -0
- package/src/v2/components/chat/CopilotSidebarView.tsx +255 -0
- package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.tsx +1113 -0
- package/src/v2/components/chat/__tests__/CopilotChat.onError.test.tsx +73 -0
- package/src/v2/components/chat/__tests__/CopilotChat.slots.e2e.test.tsx +432 -0
- package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +150 -0
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.slots.e2e.test.tsx +624 -0
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.tsx +702 -0
- package/src/v2/components/chat/__tests__/CopilotChatCssClasses.test.tsx +107 -0
- package/src/v2/components/chat/__tests__/CopilotChatInput.slots.e2e.test.tsx +929 -0
- package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +986 -0
- package/src/v2/components/chat/__tests__/CopilotChatMessageView.slots.e2e.test.tsx +1004 -0
- package/src/v2/components/chat/__tests__/CopilotChatMessageView.test.tsx +169 -0
- package/src/v2/components/chat/__tests__/CopilotChatSuggestionView.slots.e2e.test.tsx +530 -0
- package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.tsx +782 -0
- package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +2413 -0
- package/src/v2/components/chat/__tests__/CopilotChatUserMessage.slots.e2e.test.tsx +621 -0
- package/src/v2/components/chat/__tests__/CopilotChatView.onClick.e2e.test.tsx +853 -0
- package/src/v2/components/chat/__tests__/CopilotChatView.slots.e2e.test.tsx +1050 -0
- package/src/v2/components/chat/__tests__/CopilotModalHeader.slots.e2e.test.tsx +484 -0
- package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.tsx +612 -0
- package/src/v2/components/chat/__tests__/CopilotSidebarView.slots.e2e.test.tsx +502 -0
- package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +1011 -0
- package/src/v2/components/chat/__tests__/setup.ts +1 -0
- package/src/v2/components/chat/index.ts +79 -0
- package/src/v2/components/index.ts +7 -0
- package/src/v2/components/license-warning-banner.tsx +198 -0
- package/src/v2/components/ui/button.tsx +123 -0
- package/src/v2/components/ui/dropdown-menu.tsx +258 -0
- package/src/v2/components/ui/tooltip.tsx +60 -0
- package/src/v2/hooks/__tests__/standard-schema-types.test.tsx +152 -0
- package/src/v2/hooks/__tests__/standard-schema.test.tsx +282 -0
- package/src/v2/hooks/__tests__/use-agent-context-timing.e2e.test.tsx +132 -0
- package/src/v2/hooks/__tests__/use-agent-context.test.tsx +401 -0
- package/src/v2/hooks/__tests__/use-agent-error-state.test.tsx +44 -0
- package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +205 -0
- package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +148 -0
- package/src/v2/hooks/__tests__/use-component.test.tsx +123 -0
- package/src/v2/hooks/__tests__/use-configure-suggestions.e2e.test.tsx +696 -0
- package/src/v2/hooks/__tests__/use-default-render-tool.test.tsx +153 -0
- package/src/v2/hooks/__tests__/use-frontend-tool-available.test.tsx +167 -0
- package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +2129 -0
- package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.tsx +1261 -0
- package/src/v2/hooks/__tests__/use-interrupt.test.tsx +397 -0
- package/src/v2/hooks/__tests__/use-katex-styles.test.tsx +56 -0
- package/src/v2/hooks/__tests__/use-keyboard-height.test.tsx +192 -0
- package/src/v2/hooks/__tests__/use-render-tool.test.tsx +259 -0
- package/src/v2/hooks/__tests__/use-suggestions.e2e.test.tsx +524 -0
- package/src/v2/hooks/__tests__/use-threads.test.tsx +433 -0
- package/src/v2/hooks/__tests__/zod-regression.test.tsx +311 -0
- package/src/v2/hooks/index.ts +18 -0
- package/src/v2/hooks/use-agent-context.tsx +45 -0
- package/src/v2/hooks/use-agent.tsx +155 -0
- package/src/v2/hooks/use-component.tsx +89 -0
- package/src/v2/hooks/use-configure-suggestions.tsx +187 -0
- package/src/v2/hooks/use-default-render-tool.tsx +254 -0
- package/src/v2/hooks/use-frontend-tool.tsx +43 -0
- package/src/v2/hooks/use-human-in-the-loop.tsx +81 -0
- package/src/v2/hooks/use-interrupt.tsx +305 -0
- package/src/v2/hooks/use-keyboard-height.tsx +67 -0
- package/src/v2/hooks/use-render-activity-message.tsx +73 -0
- package/src/v2/hooks/use-render-custom-messages.tsx +93 -0
- package/src/v2/hooks/use-render-tool-call.tsx +175 -0
- package/src/v2/hooks/use-render-tool.tsx +181 -0
- package/src/v2/hooks/use-suggestions.tsx +91 -0
- package/src/v2/hooks/use-threads.tsx +256 -0
- package/src/v2/hooks/useKatexStyles.ts +27 -0
- package/src/v2/index.css +1 -1
- package/src/v2/index.ts +18 -2
- package/src/v2/lib/__tests__/completePartialMarkdown.test.ts +495 -0
- package/src/v2/lib/__tests__/renderSlot.test.tsx +588 -0
- package/src/v2/lib/react-core.ts +156 -0
- package/src/v2/lib/slots.tsx +143 -0
- package/src/v2/lib/transcription-client.ts +184 -0
- package/src/v2/lib/utils.ts +8 -0
- package/src/v2/providers/CopilotChatConfigurationProvider.tsx +162 -0
- package/src/v2/providers/CopilotKitProvider.tsx +600 -0
- package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.tsx +546 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.license.test.tsx +101 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.onError.test.tsx +69 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.renderCustomMessages.e2e.test.tsx +881 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.stability.test.tsx +740 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +642 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.wildcard.test.tsx +294 -0
- package/src/v2/providers/index.ts +14 -0
- package/src/v2/styles/globals.css +230 -0
- package/src/v2/types/__tests__/defineToolCallRenderer.test.tsx +525 -0
- package/src/v2/types/defineToolCallRenderer.ts +65 -0
- package/src/v2/types/frontend-tool.ts +8 -0
- package/src/v2/types/human-in-the-loop.ts +33 -0
- package/src/v2/types/index.ts +7 -0
- package/src/v2/types/interrupt.ts +15 -0
- package/src/v2/types/react-activity-message-renderer.ts +27 -0
- package/src/v2/types/react-custom-message-renderer.ts +17 -0
- package/src/v2/types/react-tool-call-renderer.ts +32 -0
- package/tsdown.config.ts +34 -10
- package/vitest.config.mjs +4 -3
- package/LICENSE +0 -21
- package/dist/copilotkit-BRPQ2sqS.d.cts +0 -670
- package/dist/copilotkit-BRPQ2sqS.d.cts.map +0 -1
- package/dist/copilotkit-C94ayZbs.cjs +0 -2161
- package/dist/copilotkit-C94ayZbs.cjs.map +0 -1
- package/dist/copilotkit-CwZMFmSK.d.mts +0 -670
- package/dist/copilotkit-CwZMFmSK.d.mts.map +0 -1
- package/dist/copilotkit-Yh_Ld_FX.mjs +0 -2031
- package/dist/copilotkit-Yh_Ld_FX.mjs.map +0 -1
- 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
|
+
});
|