@copilotkit/vue 1.75.2 → 1.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PARITY.md +93 -35
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +275 -158
- package/dist/index.mjs.map +1 -1
- package/dist/use-render-activity-message-JCCE1lgH.cjs +79 -0
- package/dist/use-render-activity-message-JCCE1lgH.cjs.map +1 -0
- package/dist/use-render-activity-message-bPlnHjQV.js +12499 -0
- package/dist/use-render-activity-message-bPlnHjQV.js.map +1 -0
- package/dist/v2/components/MCPAppsActivityRenderer.d.ts +10 -42
- package/dist/v2/components/MCPAppsActivityRenderer.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts +8 -152
- package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatView.vue.d.ts +1 -1
- package/dist/v2/components/chat/CopilotPopupViewInternal.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/copilotChatToolCallItemRender.d.ts.map +1 -1
- package/dist/v2/components/chat/index.d.ts +3 -3
- package/dist/v2/hooks/use-agent.d.ts.map +1 -1
- package/dist/v2/index.cjs +1 -1
- package/dist/v2/index.d.cts +2 -0
- package/dist/v2/index.d.mts +2 -0
- package/dist/v2/index.d.ts +2 -0
- package/dist/v2/index.d.ts.map +1 -1
- package/dist/v2/index.mjs +189 -72
- package/dist/v2/index.mjs.map +1 -1
- package/package.json +8 -7
- package/src/v2/__tests__/utils/test-helpers.ts +26 -6
- package/src/v2/components/MCPAppsActivityRenderer.ts +175 -658
- package/src/v2/components/__tests__/MCPAppsActivityRenderer.test.ts +39 -20
- package/src/v2/components/__tests__/MCPAppsActivityRendererContentError.test.ts +122 -0
- package/src/v2/components/__tests__/MCPAppsActivityRendererSeed.test.ts +109 -0
- package/src/v2/components/chat/CopilotChat.vue +100 -20
- package/src/v2/components/chat/CopilotPopupViewInternal.vue +1 -1
- package/src/v2/components/chat/__tests__/CopilotChat.clearOnFresh.test.ts +130 -2
- package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.ts +6 -1
- package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.ts +22 -40
- package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.ts +12 -24
- package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.ts +20 -0
- package/src/v2/components/chat/copilotChatToolCallItemRender.ts +3 -2
- package/src/v2/hooks/__tests__/use-agent-headers.test.ts +83 -0
- package/src/v2/hooks/__tests__/use-agent-stability.test.ts +2 -0
- package/src/v2/hooks/__tests__/use-agent.e2e.test.ts +5 -3
- package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.ts +2 -2
- package/src/v2/hooks/use-agent.ts +3 -3
- package/src/v2/index.ts +5 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.renderCustomMessages.e2e.test.ts +6 -4
- package/vite.config.ts +7 -0
- package/dist/use-render-activity-message-CGdS03Xj.cjs +0 -85
- package/dist/use-render-activity-message-CGdS03Xj.cjs.map +0 -1
- package/dist/use-render-activity-message-XmuT9fKy.js +0 -9799
- package/dist/use-render-activity-message-XmuT9fKy.js.map +0 -1
|
@@ -1,237 +1,46 @@
|
|
|
1
|
-
import { computed, defineComponent, h,
|
|
1
|
+
import { computed, defineComponent, h, ref, shallowRef, watch } from "vue";
|
|
2
2
|
import type { PropType } from "vue";
|
|
3
|
-
import {
|
|
4
|
-
import type { AbstractAgent, RunAgentResult } from "@ag-ui/client";
|
|
5
|
-
import { randomUUID } from "@copilotkit/shared";
|
|
3
|
+
import type { AbstractAgent } from "@ag-ui/client";
|
|
6
4
|
import type { VueActivityMessageRendererProps } from "../types";
|
|
7
5
|
import { useCopilotKit } from "../providers/useCopilotKit";
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
class MCPAppsRequestQueue {
|
|
53
|
-
private queues = new Map<
|
|
54
|
-
string,
|
|
55
|
-
Array<{
|
|
56
|
-
execute: () => Promise<RunAgentResult>;
|
|
57
|
-
resolve: (result: RunAgentResult) => void;
|
|
58
|
-
reject: (error: Error) => void;
|
|
59
|
-
}>
|
|
60
|
-
>();
|
|
61
|
-
|
|
62
|
-
private processing = new Map<string, boolean>();
|
|
63
|
-
|
|
64
|
-
async enqueue(
|
|
65
|
-
agent: AbstractAgent,
|
|
66
|
-
request: () => Promise<RunAgentResult>,
|
|
67
|
-
): Promise<RunAgentResult> {
|
|
68
|
-
const threadId = agent.threadId || "default";
|
|
69
|
-
|
|
70
|
-
return new Promise((resolve, reject) => {
|
|
71
|
-
let queue = this.queues.get(threadId);
|
|
72
|
-
if (!queue) {
|
|
73
|
-
queue = [];
|
|
74
|
-
this.queues.set(threadId, queue);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
queue.push({ execute: request, resolve, reject });
|
|
78
|
-
void this.processQueue(threadId, agent);
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
private async processQueue(
|
|
83
|
-
threadId: string,
|
|
84
|
-
agent: AbstractAgent,
|
|
85
|
-
): Promise<void> {
|
|
86
|
-
if (this.processing.get(threadId)) {
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
this.processing.set(threadId, true);
|
|
91
|
-
try {
|
|
92
|
-
const queue = this.queues.get(threadId);
|
|
93
|
-
if (!queue) return;
|
|
94
|
-
|
|
95
|
-
while (queue.length > 0) {
|
|
96
|
-
const item = queue[0]!;
|
|
97
|
-
try {
|
|
98
|
-
await this.waitForAgentIdle(agent);
|
|
99
|
-
const result = await item.execute();
|
|
100
|
-
item.resolve(result);
|
|
101
|
-
} catch (error) {
|
|
102
|
-
item.reject(
|
|
103
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
queue.shift();
|
|
107
|
-
}
|
|
108
|
-
} finally {
|
|
109
|
-
this.processing.set(threadId, false);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
cancel(threadId: string): void {
|
|
114
|
-
const queue = this.queues.get(threadId);
|
|
115
|
-
if (queue) {
|
|
116
|
-
for (const item of queue) {
|
|
117
|
-
item.reject(new Error("MCPAppsRequestQueue cancelled on unmount"));
|
|
118
|
-
}
|
|
119
|
-
queue.length = 0;
|
|
120
|
-
}
|
|
121
|
-
this.queues.delete(threadId);
|
|
122
|
-
this.processing.delete(threadId);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
private waitForAgentIdle(agent: AbstractAgent): Promise<void> {
|
|
126
|
-
return new Promise((resolve, reject) => {
|
|
127
|
-
if (!agent.isRunning) {
|
|
128
|
-
resolve();
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
let done = false;
|
|
133
|
-
|
|
134
|
-
const timeout = setTimeout(() => {
|
|
135
|
-
if (done) return;
|
|
136
|
-
done = true;
|
|
137
|
-
clearInterval(checkInterval);
|
|
138
|
-
sub.unsubscribe();
|
|
139
|
-
reject(
|
|
140
|
-
new Error("[CopilotKit] Timed out waiting for agent to become idle"),
|
|
141
|
-
);
|
|
142
|
-
}, 30_000);
|
|
143
|
-
|
|
144
|
-
const finish = () => {
|
|
145
|
-
if (done) return;
|
|
146
|
-
done = true;
|
|
147
|
-
clearTimeout(timeout);
|
|
148
|
-
clearInterval(checkInterval);
|
|
149
|
-
sub.unsubscribe();
|
|
150
|
-
resolve();
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const sub = agent.subscribe({
|
|
154
|
-
onRunFinalized: finish,
|
|
155
|
-
onRunFailed: finish,
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
const checkInterval = setInterval(() => {
|
|
159
|
-
if (!agent.isRunning) {
|
|
160
|
-
finish();
|
|
161
|
-
}
|
|
162
|
-
}, 500);
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const mcpAppsRequestQueue = new MCPAppsRequestQueue();
|
|
168
|
-
|
|
169
|
-
export const MCPAppsActivityType = "mcp-apps";
|
|
170
|
-
|
|
171
|
-
export const MCPAppsActivityContentSchema = z.object({
|
|
172
|
-
result: z.object({
|
|
173
|
-
content: z.array(z.any()).optional(),
|
|
174
|
-
structuredContent: z.any().optional(),
|
|
175
|
-
isError: z.boolean().optional(),
|
|
176
|
-
}),
|
|
177
|
-
resourceUri: z.string(),
|
|
178
|
-
serverHash: z.string(),
|
|
179
|
-
serverId: z.string().optional(),
|
|
180
|
-
toolInput: z.record(z.string(), z.unknown()).optional(),
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
export type MCPAppsActivityContent = z.infer<
|
|
184
|
-
typeof MCPAppsActivityContentSchema
|
|
185
|
-
>;
|
|
186
|
-
|
|
187
|
-
interface FetchedResource {
|
|
188
|
-
uri: string;
|
|
189
|
-
mimeType?: string;
|
|
190
|
-
text?: string;
|
|
191
|
-
blob?: string;
|
|
192
|
-
_meta?: {
|
|
193
|
-
ui?: {
|
|
194
|
-
prefersBorder?: boolean;
|
|
195
|
-
csp?: {
|
|
196
|
-
connectDomains?: string[];
|
|
197
|
-
resourceDomains?: string[];
|
|
198
|
-
};
|
|
199
|
-
};
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
interface JSONRPCRequest {
|
|
204
|
-
jsonrpc: "2.0";
|
|
205
|
-
id: string | number;
|
|
206
|
-
method: string;
|
|
207
|
-
params?: Record<string, unknown>;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
interface JSONRPCResponse {
|
|
211
|
-
jsonrpc: "2.0";
|
|
212
|
-
id: string | number;
|
|
213
|
-
result?: unknown;
|
|
214
|
-
error?: { code: number; message: string };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
interface JSONRPCNotification {
|
|
218
|
-
jsonrpc: "2.0";
|
|
219
|
-
method: string;
|
|
220
|
-
params?: Record<string, unknown>;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
type JSONRPCMessage = JSONRPCRequest | JSONRPCResponse | JSONRPCNotification;
|
|
224
|
-
|
|
225
|
-
function isRequest(message: JSONRPCMessage): message is JSONRPCRequest {
|
|
226
|
-
return "id" in message && "method" in message;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function isNotification(
|
|
230
|
-
message: JSONRPCMessage,
|
|
231
|
-
): message is JSONRPCNotification {
|
|
232
|
-
return !("id" in message) && "method" in message;
|
|
233
|
-
}
|
|
234
|
-
|
|
7
|
+
// The app<->host protocol (ext-apps AppBridge, sandbox proxy, request queue,
|
|
8
|
+
// ui/message + open-link handlers, tool input/result) lives in the shared,
|
|
9
|
+
// framework-agnostic package. This file is now a THIN Vue adapter over it: it
|
|
10
|
+
// owns the iframe (create/mount/size/remove) and wires the session's reactive
|
|
11
|
+
// hooks to Vue refs; all protocol logic is `bindMcpApp`.
|
|
12
|
+
//
|
|
13
|
+
// The lightweight activity surface (type + content schema + follow-up runner)
|
|
14
|
+
// is re-exported from the package's bridge-free `/activity` entry, so importing
|
|
15
|
+
// it (for the activity registry) does NOT pull the ext-apps bundle. The bridge
|
|
16
|
+
// itself is loaded lazily via a dynamic `import("@copilotkit/mcp-apps-renderer")`
|
|
17
|
+
// inside the watcher, so a `<CopilotKitProvider>` app only pays for it when it
|
|
18
|
+
// actually renders an MCP App.
|
|
19
|
+
export {
|
|
20
|
+
MCPAppsActivityType,
|
|
21
|
+
MCPAppsActivityContentSchema,
|
|
22
|
+
ɵrunMcpFollowUp,
|
|
23
|
+
} from "@copilotkit/mcp-apps-renderer/activity";
|
|
24
|
+
export type {
|
|
25
|
+
MCPAppsActivityContent,
|
|
26
|
+
ɵMcpFollowUpHost,
|
|
27
|
+
} from "@copilotkit/mcp-apps-renderer/activity";
|
|
28
|
+
|
|
29
|
+
import type { MCPAppsActivityContent } from "@copilotkit/mcp-apps-renderer/activity";
|
|
30
|
+
// Type-only imports: erased at build, so they never pull the ext-apps bridge
|
|
31
|
+
// into the bundle. Only the dynamic import() below does, and only lazily.
|
|
32
|
+
import type {
|
|
33
|
+
McpAppSession,
|
|
34
|
+
FetchedResource,
|
|
35
|
+
} from "@copilotkit/mcp-apps-renderer";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* MCP Apps Extension Activity Renderer
|
|
39
|
+
*
|
|
40
|
+
* Renders MCP Apps UI in a sandboxed iframe with full protocol support.
|
|
41
|
+
* Fetches resource content on-demand via proxied MCP requests. The Vue shell
|
|
42
|
+
* owns the iframe; `bindMcpApp` owns the protocol.
|
|
43
|
+
*/
|
|
235
44
|
export const MCPAppsActivityRenderer = defineComponent({
|
|
236
45
|
name: "MCPAppsActivityRenderer",
|
|
237
46
|
props: {
|
|
@@ -258,475 +67,188 @@ export const MCPAppsActivityRenderer = defineComponent({
|
|
|
258
67
|
setup(props) {
|
|
259
68
|
const { copilotkit } = useCopilotKit();
|
|
260
69
|
const containerRef = ref<HTMLDivElement | null>(null);
|
|
261
|
-
|
|
262
|
-
const iframeReady = ref(false);
|
|
70
|
+
// UI-only state: plain refs are fine (no agent-bound payload crosses here).
|
|
263
71
|
const error = ref<Error | null>(null);
|
|
72
|
+
// Recoverable: an activity update the content schema rejected, from the
|
|
73
|
+
// store or from props. Cleared as soon as valid content resumes, unlike
|
|
74
|
+
// `error`, which is a fatal setup failure.
|
|
75
|
+
const contentError = ref<Error | null>(null);
|
|
264
76
|
const isLoading = ref(true);
|
|
265
77
|
const iframeSize = ref<{ width?: number; height?: number }>({});
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const sendResponse = (id: string | number, result: unknown) => {
|
|
285
|
-
sendToIframe({
|
|
286
|
-
jsonrpc: "2.0",
|
|
287
|
-
id,
|
|
288
|
-
result,
|
|
289
|
-
});
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
const sendErrorResponse = (
|
|
293
|
-
id: string | number,
|
|
294
|
-
code: number,
|
|
295
|
-
message: string,
|
|
296
|
-
) => {
|
|
297
|
-
sendToIframe({
|
|
298
|
-
jsonrpc: "2.0",
|
|
299
|
-
id,
|
|
300
|
-
error: { code, message },
|
|
301
|
-
});
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
const sendNotification = (
|
|
305
|
-
method: string,
|
|
306
|
-
params?: Record<string, unknown>,
|
|
307
|
-
) => {
|
|
308
|
-
sendToIframe({
|
|
309
|
-
jsonrpc: "2.0",
|
|
310
|
-
method,
|
|
311
|
-
params: params || {},
|
|
312
|
-
});
|
|
313
|
-
};
|
|
314
|
-
|
|
78
|
+
// shallowRef for externally-owned objects: the session, the iframe element
|
|
79
|
+
// and the fetched resource must never be wrapped in a reactive proxy (the
|
|
80
|
+
// resource crosses into the widget, and proxies are not clone-safe).
|
|
81
|
+
const iframeRef = shallowRef<HTMLIFrameElement | null>(null);
|
|
82
|
+
const sessionRef = shallowRef<McpAppSession | null>(null);
|
|
83
|
+
const fetchedResource = shallowRef<FetchedResource | null>(null);
|
|
84
|
+
|
|
85
|
+
// The activity message id. Passed to the session so it self-subscribes to
|
|
86
|
+
// the agent's activity stream and pushes tool input/result itself (this
|
|
87
|
+
// adapter does not forward store-backed activities).
|
|
88
|
+
const messageId = computed(() => props.message?.id);
|
|
89
|
+
|
|
90
|
+
// Create the sandbox iframe and bind the MCP session. Re-binds only when the
|
|
91
|
+
// widget identity (resourceUri/serverHash/serverId) or the agent/host/message
|
|
92
|
+
// changes - NOT when tool input/result stream in (those are pushed without
|
|
93
|
+
// recreating the iframe). `flush: "post"` so the container element is mounted
|
|
94
|
+
// when the callback runs.
|
|
315
95
|
watch(
|
|
316
|
-
[
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
) {
|
|
330
|
-
void fetchStateRef.value.promise
|
|
331
|
-
?.then((resource) => {
|
|
332
|
-
if (resource) {
|
|
333
|
-
fetchedResource.value = resource;
|
|
334
|
-
isLoading.value = false;
|
|
335
|
-
}
|
|
336
|
-
})
|
|
337
|
-
.catch((err: unknown) => {
|
|
338
|
-
error.value = err instanceof Error ? err : new Error(String(err));
|
|
339
|
-
isLoading.value = false;
|
|
340
|
-
});
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
|
|
96
|
+
[
|
|
97
|
+
// The container element is a watch source, not just a read: `immediate`
|
|
98
|
+
// runs the callback synchronously at setup, before the template ref is
|
|
99
|
+
// populated. Watching it re-runs the bind once the element mounts.
|
|
100
|
+
containerRef,
|
|
101
|
+
() => props.agent,
|
|
102
|
+
() => copilotkit.value,
|
|
103
|
+
messageId,
|
|
104
|
+
() => props.content.resourceUri,
|
|
105
|
+
() => props.content.serverHash,
|
|
106
|
+
() => props.content.serverId,
|
|
107
|
+
],
|
|
108
|
+
([container, agent], _old, onCleanup) => {
|
|
344
109
|
if (!agent) {
|
|
345
110
|
error.value = new Error("No agent available to fetch resource");
|
|
346
111
|
isLoading.value = false;
|
|
347
112
|
return;
|
|
348
113
|
}
|
|
349
|
-
|
|
350
|
-
fetchStateRef.value.inProgress = true;
|
|
351
|
-
fetchStateRef.value.resourceUri = resourceUri;
|
|
352
|
-
|
|
353
|
-
const fetchPromise = (async (): Promise<FetchedResource | null> => {
|
|
354
|
-
try {
|
|
355
|
-
const runResult = await mcpAppsRequestQueue.enqueue(agent, () =>
|
|
356
|
-
agent.runAgent({
|
|
357
|
-
forwardedProps: {
|
|
358
|
-
__proxiedMCPRequest: {
|
|
359
|
-
serverHash,
|
|
360
|
-
serverId,
|
|
361
|
-
method: "resources/read",
|
|
362
|
-
params: { uri: resourceUri },
|
|
363
|
-
},
|
|
364
|
-
},
|
|
365
|
-
}),
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
const resultData = runResult.result as
|
|
369
|
-
| { contents?: FetchedResource[] }
|
|
370
|
-
| undefined;
|
|
371
|
-
const resource = resultData?.contents?.[0];
|
|
372
|
-
|
|
373
|
-
if (!resource) {
|
|
374
|
-
throw new Error("No resource content in response");
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
return resource;
|
|
378
|
-
} finally {
|
|
379
|
-
fetchStateRef.value.inProgress = false;
|
|
380
|
-
}
|
|
381
|
-
})();
|
|
382
|
-
|
|
383
|
-
fetchStateRef.value.promise = fetchPromise;
|
|
384
|
-
|
|
385
|
-
void fetchPromise
|
|
386
|
-
.then((resource) => {
|
|
387
|
-
if (resource) {
|
|
388
|
-
fetchedResource.value = resource;
|
|
389
|
-
isLoading.value = false;
|
|
390
|
-
}
|
|
391
|
-
})
|
|
392
|
-
.catch((err: unknown) => {
|
|
393
|
-
error.value = err instanceof Error ? err : new Error(String(err));
|
|
394
|
-
isLoading.value = false;
|
|
395
|
-
});
|
|
396
|
-
},
|
|
397
|
-
{ immediate: true },
|
|
398
|
-
);
|
|
399
|
-
|
|
400
|
-
watch(
|
|
401
|
-
[isLoading, fetchedResource],
|
|
402
|
-
([loading, resource], _old, onCleanup) => {
|
|
403
|
-
if (loading || !resource) {
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const container = containerRef.value;
|
|
408
114
|
if (!container) {
|
|
409
115
|
return;
|
|
410
116
|
}
|
|
411
117
|
|
|
412
118
|
let mounted = true;
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
119
|
+
isLoading.value = true;
|
|
120
|
+
error.value = null;
|
|
121
|
+
contentError.value = null;
|
|
122
|
+
iframeSize.value = {};
|
|
123
|
+
fetchedResource.value = null;
|
|
124
|
+
|
|
125
|
+
// The host owns the iframe: create + mount it here (bindMcpApp only
|
|
126
|
+
// configures the sandbox contract + talks to it through the bridge).
|
|
127
|
+
const iframe = document.createElement("iframe");
|
|
128
|
+
iframe.style.width = "100%";
|
|
129
|
+
iframe.style.height = "100px";
|
|
130
|
+
iframe.style.border = "none";
|
|
131
|
+
iframe.style.backgroundColor = "transparent";
|
|
132
|
+
iframe.style.display = "block";
|
|
133
|
+
container.appendChild(iframe);
|
|
134
|
+
iframeRef.value = iframe;
|
|
135
|
+
|
|
136
|
+
// Register cleanup BEFORE the async import: if this watcher is
|
|
137
|
+
// invalidated (props change / unmount) while the import is in flight,
|
|
138
|
+
// `mounted` is already false when it resolves, so we never bind.
|
|
139
|
+
onCleanup(() => {
|
|
140
|
+
mounted = false;
|
|
141
|
+
sessionRef.value?.teardown();
|
|
142
|
+
sessionRef.value = null;
|
|
143
|
+
iframe.remove();
|
|
144
|
+
if (iframeRef.value === iframe) {
|
|
145
|
+
iframeRef.value = null;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
416
148
|
|
|
417
149
|
const setup = async () => {
|
|
418
150
|
try {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
151
|
+
// Load the bridge package lazily. The bridge is heavy (it pulls the
|
|
152
|
+
// MCP SDK Protocol + zod schemas, ~40-50 kB gzipped); keeping it
|
|
153
|
+
// behind a dynamic import() means a non-MCP app never pays for it.
|
|
154
|
+
const mod = await import("@copilotkit/mcp-apps-renderer").catch(
|
|
155
|
+
(importErr) => {
|
|
156
|
+
throw new Error(
|
|
157
|
+
"MCP Apps require '@copilotkit/mcp-apps-renderer' and its " +
|
|
158
|
+
"'@modelcontextprotocol/ext-apps' dependency. Reinstall your " +
|
|
159
|
+
"dependencies if this package is missing.",
|
|
160
|
+
{ cause: importErr },
|
|
161
|
+
);
|
|
162
|
+
},
|
|
429
163
|
);
|
|
430
|
-
// Cross-frontend MCP-apps surface contract — same testid/title pair
|
|
431
|
-
// as react-core's MCPAppsActivityRenderer and Angular's
|
|
432
|
-
// copilot-mcp-apps-widget, so the shared harness probe can assert
|
|
433
|
-
// the surface mounted with one selector on every frontend.
|
|
434
|
-
iframe.setAttribute("data-testid", "mcp-app-iframe");
|
|
435
|
-
iframe.setAttribute("title", "Interactive MCP application");
|
|
436
|
-
|
|
437
|
-
const sandboxReady = new Promise<void>((resolve) => {
|
|
438
|
-
initialListener = (event: MessageEvent) => {
|
|
439
|
-
if (
|
|
440
|
-
event.source === iframe.contentWindow &&
|
|
441
|
-
event.data?.method === "ui/notifications/sandbox-proxy-ready"
|
|
442
|
-
) {
|
|
443
|
-
if (initialListener) {
|
|
444
|
-
window.removeEventListener("message", initialListener);
|
|
445
|
-
initialListener = null;
|
|
446
|
-
}
|
|
447
|
-
resolve();
|
|
448
|
-
}
|
|
449
|
-
};
|
|
450
|
-
window.addEventListener("message", initialListener);
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
if (!mounted) {
|
|
454
|
-
if (initialListener) {
|
|
455
|
-
window.removeEventListener("message", initialListener);
|
|
456
|
-
initialListener = null;
|
|
457
|
-
}
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const cspDomains =
|
|
462
|
-
fetchedResource.value?._meta?.ui?.csp?.resourceDomains;
|
|
463
|
-
iframe.srcdoc = buildSandboxHTML(cspDomains);
|
|
464
|
-
iframeRef.value = iframe;
|
|
465
|
-
container.appendChild(iframe);
|
|
466
|
-
|
|
467
|
-
await sandboxReady;
|
|
468
164
|
if (!mounted) return;
|
|
469
165
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
case "ui/message": {
|
|
500
|
-
const currentAgent = props.agent;
|
|
501
|
-
if (!currentAgent) {
|
|
502
|
-
sendResponse(msg.id, { isError: false });
|
|
503
|
-
break;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
try {
|
|
507
|
-
const params = msg.params as
|
|
508
|
-
| {
|
|
509
|
-
role?: string;
|
|
510
|
-
content?: Array<{ type: string; text?: string }>;
|
|
511
|
-
followUp?: boolean;
|
|
512
|
-
}
|
|
513
|
-
| undefined;
|
|
514
|
-
|
|
515
|
-
const role =
|
|
516
|
-
(params?.role as "user" | "assistant") || "user";
|
|
517
|
-
const textContent =
|
|
518
|
-
params?.content
|
|
519
|
-
?.filter((part) => part.type === "text" && part.text)
|
|
520
|
-
.map((part) => part.text)
|
|
521
|
-
.join("\n") || "";
|
|
522
|
-
|
|
523
|
-
if (textContent) {
|
|
524
|
-
currentAgent.addMessage({
|
|
525
|
-
id: randomUUID(),
|
|
526
|
-
role,
|
|
527
|
-
content: textContent,
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
sendResponse(msg.id, { isError: false });
|
|
532
|
-
|
|
533
|
-
const shouldFollowUp =
|
|
534
|
-
params?.followUp ?? role === "user";
|
|
535
|
-
if (shouldFollowUp && textContent) {
|
|
536
|
-
void mcpAppsRequestQueue
|
|
537
|
-
.enqueue(currentAgent, () =>
|
|
538
|
-
copilotkit.value.runAgent({ agent: currentAgent }),
|
|
539
|
-
)
|
|
540
|
-
.catch((err) => {
|
|
541
|
-
console.error(
|
|
542
|
-
"[MCPAppsRenderer] ui/message agent run failed:",
|
|
543
|
-
err,
|
|
544
|
-
);
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
} catch (err) {
|
|
548
|
-
console.error(
|
|
549
|
-
"[CopilotKit] MCPApps ui/message handler error:",
|
|
550
|
-
err,
|
|
551
|
-
);
|
|
552
|
-
sendResponse(msg.id, { isError: true });
|
|
553
|
-
}
|
|
554
|
-
break;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
case "ui/open-link": {
|
|
558
|
-
const url = msg.params?.url as string | undefined;
|
|
559
|
-
if (!url) {
|
|
560
|
-
sendErrorResponse(
|
|
561
|
-
msg.id,
|
|
562
|
-
-32602,
|
|
563
|
-
"Missing url parameter",
|
|
564
|
-
);
|
|
565
|
-
break;
|
|
566
|
-
}
|
|
567
|
-
window.open(url, "_blank", "noopener,noreferrer");
|
|
568
|
-
sendResponse(msg.id, { isError: false });
|
|
569
|
-
break;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
case "tools/call": {
|
|
573
|
-
const { serverHash, serverId } = props.content;
|
|
574
|
-
const currentAgent = props.agent;
|
|
575
|
-
|
|
576
|
-
if (!serverHash) {
|
|
577
|
-
sendErrorResponse(
|
|
578
|
-
msg.id,
|
|
579
|
-
-32603,
|
|
580
|
-
"No server hash available for proxying",
|
|
581
|
-
);
|
|
582
|
-
break;
|
|
583
|
-
}
|
|
584
|
-
if (!currentAgent) {
|
|
585
|
-
sendErrorResponse(
|
|
586
|
-
msg.id,
|
|
587
|
-
-32603,
|
|
588
|
-
"No agent available for proxying",
|
|
589
|
-
);
|
|
590
|
-
break;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
try {
|
|
594
|
-
const runResult = await mcpAppsRequestQueue.enqueue(
|
|
595
|
-
currentAgent,
|
|
596
|
-
() =>
|
|
597
|
-
currentAgent.runAgent({
|
|
598
|
-
forwardedProps: {
|
|
599
|
-
__proxiedMCPRequest: {
|
|
600
|
-
serverHash,
|
|
601
|
-
serverId,
|
|
602
|
-
method: "tools/call",
|
|
603
|
-
params: msg.params,
|
|
604
|
-
},
|
|
605
|
-
},
|
|
606
|
-
}),
|
|
607
|
-
);
|
|
608
|
-
sendResponse(msg.id, runResult.result || {});
|
|
609
|
-
} catch (err) {
|
|
610
|
-
sendErrorResponse(msg.id, -32603, String(err));
|
|
611
|
-
}
|
|
612
|
-
break;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
default: {
|
|
616
|
-
sendErrorResponse(
|
|
617
|
-
msg.id,
|
|
618
|
-
-32601,
|
|
619
|
-
`Method not found: ${msg.method}`,
|
|
620
|
-
);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
if (isNotification(msg)) {
|
|
626
|
-
switch (msg.method) {
|
|
627
|
-
case "ui/notifications/initialized":
|
|
628
|
-
if (mounted) {
|
|
629
|
-
iframeReady.value = true;
|
|
630
|
-
}
|
|
631
|
-
break;
|
|
632
|
-
|
|
633
|
-
case "ui/notifications/size-changed": {
|
|
634
|
-
const { width, height } = msg.params || {};
|
|
635
|
-
if (mounted) {
|
|
636
|
-
iframeSize.value = {
|
|
637
|
-
width: typeof width === "number" ? width : undefined,
|
|
638
|
-
height: typeof height === "number" ? height : undefined,
|
|
639
|
-
};
|
|
640
|
-
}
|
|
641
|
-
break;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
};
|
|
646
|
-
|
|
647
|
-
window.addEventListener("message", messageHandler);
|
|
648
|
-
|
|
649
|
-
const html = resource.text
|
|
650
|
-
? resource.text
|
|
651
|
-
: resource.blob
|
|
652
|
-
? atob(resource.blob)
|
|
653
|
-
: null;
|
|
654
|
-
|
|
655
|
-
if (!html) {
|
|
656
|
-
throw new Error("Resource has no text or blob content");
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
sendNotification("ui/notifications/sandbox-resource-ready", {
|
|
660
|
-
html,
|
|
166
|
+
const session = mod.bindMcpApp({
|
|
167
|
+
iframe,
|
|
168
|
+
getContent: () => props.content,
|
|
169
|
+
getAgent: () => props.agent,
|
|
170
|
+
host: copilotkit.value,
|
|
171
|
+
// Self-driving: the session subscribes to the agent's activity
|
|
172
|
+
// stream (filtered by messageId) and pushes tool input/result to
|
|
173
|
+
// the widget itself, so this adapter does not forward STORE-backed
|
|
174
|
+
// activities. It still calls syncContent for activities rendered
|
|
175
|
+
// from an external messages list (see the seed below).
|
|
176
|
+
messageId: messageId.value,
|
|
177
|
+
hooks: {
|
|
178
|
+
onResource: (resource) => {
|
|
179
|
+
if (!mounted) return;
|
|
180
|
+
fetchedResource.value = resource;
|
|
181
|
+
isLoading.value = false;
|
|
182
|
+
},
|
|
183
|
+
onSizeChanged: (size) => {
|
|
184
|
+
if (mounted) iframeSize.value = size;
|
|
185
|
+
},
|
|
186
|
+
onContentError: (err) => {
|
|
187
|
+
if (mounted) contentError.value = err;
|
|
188
|
+
},
|
|
189
|
+
onError: (err) => {
|
|
190
|
+
if (!mounted) return;
|
|
191
|
+
error.value = err;
|
|
192
|
+
isLoading.value = false;
|
|
193
|
+
},
|
|
194
|
+
},
|
|
661
195
|
});
|
|
196
|
+
sessionRef.value = session;
|
|
197
|
+
// Seed the initial content now: the content watcher below does not
|
|
198
|
+
// re-run for unchanged props, so an activity rendered from an
|
|
199
|
+
// EXTERNAL messages list (absent from agent.messages) would never
|
|
200
|
+
// receive its initial tool input/result. Deduped + store precedence
|
|
201
|
+
// make this a no-op for agent-backed activities.
|
|
202
|
+
session.syncContent(props.content);
|
|
662
203
|
} catch (err) {
|
|
204
|
+
console.error("[MCPAppsRenderer] Setup error:", err);
|
|
663
205
|
if (mounted) {
|
|
664
206
|
error.value = err instanceof Error ? err : new Error(String(err));
|
|
207
|
+
isLoading.value = false;
|
|
665
208
|
}
|
|
666
209
|
}
|
|
667
210
|
};
|
|
668
211
|
|
|
669
212
|
void setup();
|
|
670
|
-
|
|
671
|
-
onCleanup(() => {
|
|
672
|
-
mounted = false;
|
|
673
|
-
if (initialListener) {
|
|
674
|
-
window.removeEventListener("message", initialListener);
|
|
675
|
-
initialListener = null;
|
|
676
|
-
}
|
|
677
|
-
if (messageHandler) {
|
|
678
|
-
window.removeEventListener("message", messageHandler);
|
|
679
|
-
}
|
|
680
|
-
if (createdIframe) {
|
|
681
|
-
createdIframe.remove();
|
|
682
|
-
createdIframe = null;
|
|
683
|
-
}
|
|
684
|
-
iframeRef.value = null;
|
|
685
|
-
});
|
|
686
213
|
},
|
|
687
|
-
{ flush: "post" },
|
|
214
|
+
{ immediate: true, flush: "post" },
|
|
688
215
|
);
|
|
689
216
|
|
|
217
|
+
// Size the iframe when the widget reports a new content size.
|
|
690
218
|
watch(
|
|
691
219
|
iframeSize,
|
|
692
220
|
(size) => {
|
|
693
|
-
|
|
221
|
+
const iframe = iframeRef.value;
|
|
222
|
+
if (!iframe) return;
|
|
694
223
|
if (size.width !== undefined) {
|
|
695
|
-
|
|
696
|
-
|
|
224
|
+
// Use minWidth with min() to allow expansion but cap at 100%
|
|
225
|
+
iframe.style.minWidth = `min(${size.width}px, 100%)`;
|
|
226
|
+
iframe.style.width = "100%";
|
|
697
227
|
}
|
|
698
228
|
if (size.height !== undefined) {
|
|
699
|
-
|
|
700
|
-
}
|
|
701
|
-
},
|
|
702
|
-
{ deep: true },
|
|
703
|
-
);
|
|
704
|
-
|
|
705
|
-
watch(
|
|
706
|
-
[iframeReady, () => props.content.toolInput],
|
|
707
|
-
([ready, toolInput]) => {
|
|
708
|
-
if (ready && toolInput) {
|
|
709
|
-
sendNotification("ui/notifications/tool-input", {
|
|
710
|
-
arguments: toolInput,
|
|
711
|
-
});
|
|
229
|
+
iframe.style.height = `${size.height}px`;
|
|
712
230
|
}
|
|
713
231
|
},
|
|
714
232
|
{ deep: true },
|
|
715
233
|
);
|
|
716
234
|
|
|
235
|
+
// Forward tool input/result from the content prop. The session is
|
|
236
|
+
// self-driving for activities that live in the agent's message store, but a
|
|
237
|
+
// host can render an activity from an EXTERNAL messages list that is absent
|
|
238
|
+
// from `agent.messages`; this keeps such widgets fed. `syncContent` is
|
|
239
|
+
// deduped and shares the subscription's dedup, so the agent-driven path
|
|
240
|
+
// never double-sends.
|
|
717
241
|
watch(
|
|
718
|
-
[
|
|
719
|
-
(
|
|
720
|
-
|
|
721
|
-
sendNotification(
|
|
722
|
-
"ui/notifications/tool-result",
|
|
723
|
-
result as Record<string, unknown>,
|
|
724
|
-
);
|
|
725
|
-
}
|
|
242
|
+
[() => props.content.toolInput, () => props.content.result],
|
|
243
|
+
() => {
|
|
244
|
+
sessionRef.value?.syncContent(props.content);
|
|
726
245
|
},
|
|
727
246
|
{ deep: true },
|
|
728
247
|
);
|
|
729
248
|
|
|
249
|
+
// Determine border styling based on prefersBorder metadata from fetched
|
|
250
|
+
// resource: true = show border/background, false = none, undefined = host
|
|
251
|
+
// decides (we default to none).
|
|
730
252
|
const borderStyle = computed(() => {
|
|
731
253
|
const prefersBorder = fetchedResource.value?._meta?.ui?.prefersBorder;
|
|
732
254
|
if (prefersBorder !== true) return {};
|
|
@@ -737,11 +259,6 @@ export const MCPAppsActivityRenderer = defineComponent({
|
|
|
737
259
|
};
|
|
738
260
|
});
|
|
739
261
|
|
|
740
|
-
onBeforeUnmount(() => {
|
|
741
|
-
const threadId = props.agent?.threadId || "default";
|
|
742
|
-
mcpAppsRequestQueue.cancel(threadId);
|
|
743
|
-
});
|
|
744
|
-
|
|
745
262
|
return () =>
|
|
746
263
|
h(
|
|
747
264
|
"div",
|
|
@@ -767,11 +284,11 @@ export const MCPAppsActivityRenderer = defineComponent({
|
|
|
767
284
|
"Loading...",
|
|
768
285
|
)
|
|
769
286
|
: null,
|
|
770
|
-
error.value
|
|
287
|
+
error.value || contentError.value
|
|
771
288
|
? h(
|
|
772
289
|
"div",
|
|
773
290
|
{ style: { color: "red", padding: "1rem" } },
|
|
774
|
-
`Error: ${error.value.message}`,
|
|
291
|
+
`Error: ${(error.value ?? contentError.value)!.message}`,
|
|
775
292
|
)
|
|
776
293
|
: null,
|
|
777
294
|
],
|