@copilotkit/vue 1.76.0 → 1.77.1

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 (30) hide show
  1. package/PARITY.md +93 -35
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.mjs +124 -122
  5. package/dist/index.mjs.map +1 -1
  6. package/dist/{use-render-activity-message-Eu6R2vGT.js → use-render-activity-message-C5XGgWI-.js} +4317 -4628
  7. package/dist/use-render-activity-message-C5XGgWI-.js.map +1 -0
  8. package/dist/use-render-activity-message-N-zuj24U.cjs +70 -0
  9. package/dist/use-render-activity-message-N-zuj24U.cjs.map +1 -0
  10. package/dist/v2/components/MCPAppsActivityRenderer.d.ts +10 -42
  11. package/dist/v2/components/MCPAppsActivityRenderer.d.ts.map +1 -1
  12. package/dist/v2/components/chat/CopilotChatView.vue.d.ts +1 -1
  13. package/dist/v2/components/chat/CopilotPopupViewInternal.vue.d.ts.map +1 -1
  14. package/dist/v2/components/chat/index.d.ts +3 -3
  15. package/dist/v2/index.cjs +1 -1
  16. package/dist/v2/index.mjs +125 -123
  17. package/dist/v2/index.mjs.map +1 -1
  18. package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
  19. package/package.json +8 -7
  20. package/src/v2/components/MCPAppsActivityRenderer.ts +175 -658
  21. package/src/v2/components/__tests__/MCPAppsActivityRenderer.test.ts +39 -20
  22. package/src/v2/components/__tests__/MCPAppsActivityRendererContentError.test.ts +122 -0
  23. package/src/v2/components/__tests__/MCPAppsActivityRendererSeed.test.ts +109 -0
  24. package/src/v2/components/chat/CopilotPopupViewInternal.vue +1 -1
  25. package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.ts +20 -0
  26. package/src/v2/providers/CopilotKitProvider.vue +1 -14
  27. package/vite.config.ts +7 -0
  28. package/dist/use-render-activity-message-BTKmd3f3.cjs +0 -110
  29. package/dist/use-render-activity-message-BTKmd3f3.cjs.map +0 -1
  30. package/dist/use-render-activity-message-Eu6R2vGT.js.map +0 -1
@@ -1,237 +1,46 @@
1
- import { computed, defineComponent, h, onBeforeUnmount, ref, watch } from "vue";
1
+ import { computed, defineComponent, h, ref, shallowRef, watch } from "vue";
2
2
  import type { PropType } from "vue";
3
- import { z } from "zod";
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
- const PROTOCOL_VERSION = "2025-06-18";
10
-
11
- function buildSandboxHTML(extraCspDomains?: string[]): string {
12
- const baseScriptSrc =
13
- "'self' 'wasm-unsafe-eval' 'unsafe-inline' 'unsafe-eval' blob: data: http://localhost:* https://localhost:*";
14
- const baseFrameSrc = "* blob: data: http://localhost:* https://localhost:*";
15
- const extra = extraCspDomains?.length ? ` ${extraCspDomains.join(" ")}` : "";
16
- const scriptSrc = `${baseScriptSrc}${extra}`;
17
- const frameSrc = `${baseFrameSrc}${extra}`;
18
-
19
- return `<!doctype html>
20
- <html>
21
- <head>
22
- <meta charset="utf-8" />
23
- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src * data: blob: 'unsafe-inline'; media-src * blob: data:; font-src * blob: data:; script-src ${scriptSrc}; style-src * blob: data: 'unsafe-inline'; connect-src *; frame-src ${frameSrc}; base-uri 'self';" />
24
- <style>html,body{margin:0;padding:0;height:100%;width:100%;overflow:hidden}*{box-sizing:border-box}iframe{background-color:transparent;border:none;padding:0;overflow:hidden;width:100%;height:100%}</style>
25
- </head>
26
- <body>
27
- <script>
28
- if(window.self===window.top){throw new Error("This file must be used in an iframe.")}
29
- const inner=document.createElement("iframe");
30
- inner.style="width:100%;height:100%;border:none;";
31
- inner.setAttribute("sandbox","allow-scripts allow-same-origin allow-forms");
32
- document.body.appendChild(inner);
33
- window.addEventListener("message",async(event)=>{
34
- if(event.source===window.parent){
35
- if(event.data&&event.data.method==="ui/notifications/sandbox-resource-ready"){
36
- const{html,sandbox}=event.data.params;
37
- if(typeof sandbox==="string")inner.setAttribute("sandbox",sandbox);
38
- if(typeof html==="string")inner.srcdoc=html;
39
- }else if(inner&&inner.contentWindow){
40
- inner.contentWindow.postMessage(event.data,"*");
41
- }
42
- }else if(event.source===inner.contentWindow){
43
- window.parent.postMessage(event.data,"*");
44
- }
45
- });
46
- window.parent.postMessage({jsonrpc:"2.0",method:"ui/notifications/sandbox-proxy-ready",params:{}},"*");
47
- </script>
48
- </body>
49
- </html>`;
50
- }
51
-
52
- 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
- const iframeRef = ref<HTMLIFrameElement | null>(null);
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
- const fetchedResource = ref<FetchedResource | null>(null);
267
-
268
- const fetchStateRef = ref<{
269
- inProgress: boolean;
270
- promise: Promise<FetchedResource | null> | null;
271
- resourceUri: string | null;
272
- }>({
273
- inProgress: false,
274
- promise: null,
275
- resourceUri: null,
276
- });
277
-
278
- const sendToIframe = (message: JSONRPCMessage) => {
279
- if (iframeRef.value?.contentWindow) {
280
- iframeRef.value.contentWindow.postMessage(message, "*");
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
- [() => props.agent, () => props.content],
317
- ([agent, content]) => {
318
- isLoading.value = true;
319
- error.value = null;
320
- iframeReady.value = false;
321
- iframeSize.value = {};
322
- fetchedResource.value = null;
323
-
324
- const { resourceUri, serverHash, serverId } = content;
325
-
326
- if (
327
- fetchStateRef.value.inProgress &&
328
- fetchStateRef.value.resourceUri === resourceUri
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
- let messageHandler: ((event: MessageEvent) => void) | null = null;
414
- let initialListener: ((event: MessageEvent) => void) | null = null;
415
- let createdIframe: HTMLIFrameElement | null = null;
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
- const iframe = document.createElement("iframe");
420
- createdIframe = iframe;
421
- iframe.style.width = "100%";
422
- iframe.style.height = "100px";
423
- iframe.style.border = "none";
424
- iframe.style.backgroundColor = "transparent";
425
- iframe.style.display = "block";
426
- iframe.setAttribute(
427
- "sandbox",
428
- "allow-scripts allow-same-origin allow-forms",
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
- messageHandler = async (event: MessageEvent) => {
471
- if (event.source !== iframe.contentWindow) return;
472
-
473
- const msg = event.data as JSONRPCMessage;
474
- if (!msg || typeof msg !== "object" || msg.jsonrpc !== "2.0") {
475
- return;
476
- }
477
-
478
- if (isRequest(msg)) {
479
- switch (msg.method) {
480
- case "ui/initialize": {
481
- sendResponse(msg.id, {
482
- protocolVersion: PROTOCOL_VERSION,
483
- hostInfo: {
484
- name: "CopilotKit MCP Apps Host",
485
- version: "1.0.0",
486
- },
487
- hostCapabilities: {
488
- openLinks: {},
489
- logging: {},
490
- },
491
- hostContext: {
492
- theme: "light",
493
- platform: "web",
494
- },
495
- });
496
- break;
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
- if (!iframeRef.value) return;
221
+ const iframe = iframeRef.value;
222
+ if (!iframe) return;
694
223
  if (size.width !== undefined) {
695
- iframeRef.value.style.minWidth = `min(${size.width}px, 100%)`;
696
- iframeRef.value.style.width = "100%";
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
- iframeRef.value.style.height = `${size.height}px`;
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
- [iframeReady, () => props.content.result],
719
- ([ready, result]) => {
720
- if (ready && result) {
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
  ],