@copilotkit/vue 1.76.0 → 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.
Files changed (28) 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-JCCE1lgH.cjs +79 -0
  7. package/dist/use-render-activity-message-JCCE1lgH.cjs.map +1 -0
  8. package/dist/{use-render-activity-message-Eu6R2vGT.js → use-render-activity-message-bPlnHjQV.js} +4292 -4594
  9. package/dist/use-render-activity-message-bPlnHjQV.js.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/package.json +6 -5
  19. package/src/v2/components/MCPAppsActivityRenderer.ts +175 -658
  20. package/src/v2/components/__tests__/MCPAppsActivityRenderer.test.ts +39 -20
  21. package/src/v2/components/__tests__/MCPAppsActivityRendererContentError.test.ts +122 -0
  22. package/src/v2/components/__tests__/MCPAppsActivityRendererSeed.test.ts +109 -0
  23. package/src/v2/components/chat/CopilotPopupViewInternal.vue +1 -1
  24. package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.ts +20 -0
  25. package/vite.config.ts +7 -0
  26. package/dist/use-render-activity-message-BTKmd3f3.cjs +0 -110
  27. package/dist/use-render-activity-message-BTKmd3f3.cjs.map +0 -1
  28. package/dist/use-render-activity-message-Eu6R2vGT.js.map +0 -1
@@ -48,6 +48,32 @@ async function flushAsync() {
48
48
  await nextTick();
49
49
  }
50
50
 
51
+ /**
52
+ * Wait for the sandbox iframe to be configured by the shared session. The host
53
+ * mounts the iframe synchronously, but the bridge package is loaded through a
54
+ * dynamic import and the resource is fetched through the agent, so `srcdoc` (and
55
+ * the sandbox contract attributes) land a few macrotasks later.
56
+ */
57
+ async function waitForSandboxIframe(wrapper: {
58
+ find: (selector: string) => { exists: () => boolean; element: Element };
59
+ }): Promise<HTMLIFrameElement> {
60
+ let iframe: HTMLIFrameElement | undefined;
61
+ await vi.waitFor(
62
+ () => {
63
+ const found = wrapper.find("iframe");
64
+ expect(found.exists()).toBe(true);
65
+ const element = found.element as HTMLIFrameElement;
66
+ expect(element.srcdoc).toBeTruthy();
67
+ iframe = element;
68
+ },
69
+ // The bridge arrives through a dynamic import, so this waits on real module
70
+ // loading rather than a microtask. `vi.waitFor`'s 1s default is enough in
71
+ // isolation but not when the whole monorepo's suites run in parallel.
72
+ { timeout: 10_000, interval: 50 },
73
+ );
74
+ return iframe!;
75
+ }
76
+
51
77
  describe("MCPAppsActivityRenderer", () => {
52
78
  it("exports the expected activity type and schema", () => {
53
79
  expect(MCPAppsActivityType).toBe("mcp-apps");
@@ -80,8 +106,8 @@ describe("MCPAppsActivityRenderer", () => {
80
106
  },
81
107
  message: {
82
108
  id: "activity-1",
83
- role: "assistant",
84
- content: "",
109
+ role: "activity",
110
+ content: {},
85
111
  activityType: MCPAppsActivityType,
86
112
  },
87
113
  agent: undefined,
@@ -123,8 +149,8 @@ describe("MCPAppsActivityRenderer", () => {
123
149
  },
124
150
  message: {
125
151
  id: "activity-no-border",
126
- role: "assistant",
127
- content: "",
152
+ role: "activity",
153
+ content: {},
128
154
  activityType: MCPAppsActivityType,
129
155
  },
130
156
  agent,
@@ -174,18 +200,15 @@ describe("MCPAppsActivityRenderer", () => {
174
200
  },
175
201
  message: {
176
202
  id: "activity-csp",
177
- role: "assistant",
178
- content: "",
203
+ role: "activity",
204
+ content: {},
179
205
  activityType: MCPAppsActivityType,
180
206
  },
181
207
  agent,
182
208
  },
183
209
  });
184
210
 
185
- await flushAsync();
186
- await flushAsync();
187
-
188
- const iframe = wrapper.find("iframe").element as HTMLIFrameElement;
211
+ const iframe = await waitForSandboxIframe(wrapper);
189
212
  expect(iframe.srcdoc).toContain("script-src");
190
213
  expect(iframe.srcdoc).toContain("frame-src");
191
214
  expect(iframe.srcdoc).toContain("https://widgets.example.com");
@@ -214,18 +237,15 @@ describe("MCPAppsActivityRenderer", () => {
214
237
  },
215
238
  message: {
216
239
  id: "activity-default-csp",
217
- role: "assistant",
218
- content: "",
240
+ role: "activity",
241
+ content: {},
219
242
  activityType: MCPAppsActivityType,
220
243
  },
221
244
  agent,
222
245
  },
223
246
  });
224
247
 
225
- await flushAsync();
226
- await flushAsync();
227
-
228
- const iframe = wrapper.find("iframe").element as HTMLIFrameElement;
248
+ const iframe = await waitForSandboxIframe(wrapper);
229
249
  expect(iframe.srcdoc).toContain(
230
250
  "script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline' 'unsafe-eval' blob: data: http://localhost:* https://localhost:*;",
231
251
  );
@@ -260,16 +280,15 @@ describe("MCPAppsActivityRenderer", () => {
260
280
  },
261
281
  message: {
262
282
  id: "activity-testid",
263
- role: "assistant",
264
- content: "",
283
+ role: "activity",
284
+ content: {},
265
285
  activityType: MCPAppsActivityType,
266
286
  },
267
287
  agent,
268
288
  },
269
289
  });
270
290
 
271
- await flushAsync();
272
- await flushAsync();
291
+ await waitForSandboxIframe(wrapper);
273
292
 
274
293
  const iframe = wrapper.find('iframe[data-testid="mcp-app-iframe"]');
275
294
  expect(iframe.exists()).toBe(true);
@@ -0,0 +1,122 @@
1
+ /**
2
+ * A content rejection is RECOVERABLE: the session reports it through
3
+ * `onContentError` and, once valid content resumes, calls it again with `null`.
4
+ * The adapter must clear the message then - otherwise the widget starts working
5
+ * again while the host still shows a stale error until the next bind.
6
+ */
7
+ import { describe, expect, it, vi, beforeEach } from "vitest";
8
+ import { nextTick } from "vue";
9
+ import { mount } from "@vue/test-utils";
10
+ import type { AbstractAgent } from "@ag-ui/client";
11
+
12
+ // Capture the hooks the adapter hands to the session, so the test can drive the
13
+ // recoverable/fatal channels directly.
14
+ const { bindMcpAppSpy, sessionSpy } = vi.hoisted(() => {
15
+ const session = {
16
+ sendToolInput: vi.fn(),
17
+ sendToolResult: vi.fn(),
18
+ syncContent: vi.fn(),
19
+ teardown: vi.fn(),
20
+ };
21
+ return { bindMcpAppSpy: vi.fn(() => session), sessionSpy: session };
22
+ });
23
+
24
+ vi.mock("@copilotkit/mcp-apps-renderer", () => ({
25
+ bindMcpApp: bindMcpAppSpy,
26
+ }));
27
+
28
+ vi.mock("../../providers/useCopilotKit", () => ({
29
+ useCopilotKit: () => ({
30
+ copilotkit: { value: { runAgent: vi.fn() } },
31
+ }),
32
+ }));
33
+
34
+ import {
35
+ MCPAppsActivityRenderer,
36
+ MCPAppsActivityType,
37
+ } from "../MCPAppsActivityRenderer";
38
+
39
+ function createAgentMock(): AbstractAgent {
40
+ return {
41
+ threadId: "content-error-thread",
42
+ isRunning: false,
43
+ messages: [],
44
+ runAgent: vi.fn().mockResolvedValue({ result: { contents: [] } }),
45
+ addMessage: vi.fn(),
46
+ subscribe: vi.fn(() => ({ unsubscribe: vi.fn() })),
47
+ } as unknown as AbstractAgent;
48
+ }
49
+
50
+ function mountRenderer() {
51
+ return mount(MCPAppsActivityRenderer, {
52
+ props: {
53
+ activityType: MCPAppsActivityType,
54
+ content: {
55
+ resourceUri: "ui://server/app",
56
+ serverHash: "hash",
57
+ result: { content: [] },
58
+ },
59
+ message: {
60
+ id: "activity-1",
61
+ role: "activity",
62
+ content: {},
63
+ activityType: MCPAppsActivityType,
64
+ },
65
+ agent: createAgentMock(),
66
+ },
67
+ });
68
+ }
69
+
70
+ /** The hooks object passed to the most recent bindMcpApp call. */
71
+ async function capturedHooks() {
72
+ await vi.waitFor(() => expect(bindMcpAppSpy).toHaveBeenCalled());
73
+ const options = bindMcpAppSpy.mock.calls.at(-1)![0] as {
74
+ hooks?: {
75
+ onContentError?: (err: Error | null) => void;
76
+ onError?: (err: Error) => void;
77
+ };
78
+ };
79
+ return options.hooks!;
80
+ }
81
+
82
+ describe("MCPAppsActivityRenderer content error recovery", () => {
83
+ beforeEach(() => {
84
+ vi.clearAllMocks();
85
+ });
86
+
87
+ it("shows a rejected-content error and clears it when valid content resumes", async () => {
88
+ const wrapper = mountRenderer();
89
+ const hooks = await capturedHooks();
90
+
91
+ // valid -> nothing shown
92
+ expect(wrapper.text()).not.toContain("Error:");
93
+
94
+ // invalid -> the rejection is visible
95
+ hooks.onContentError!(new Error("content does not match the schema"));
96
+ await nextTick();
97
+ expect(wrapper.text()).toContain("content does not match the schema");
98
+
99
+ // valid again -> the message disappears
100
+ hooks.onContentError!(null);
101
+ await nextTick();
102
+ expect(wrapper.text()).not.toContain("content does not match the schema");
103
+ expect(wrapper.text()).not.toContain("Error:");
104
+ });
105
+
106
+ it("keeps a fatal session error on screen", async () => {
107
+ const wrapper = mountRenderer();
108
+ const hooks = await capturedHooks();
109
+
110
+ hooks.onError!(new Error("No resource content in response"));
111
+ await nextTick();
112
+ expect(wrapper.text()).toContain("No resource content in response");
113
+
114
+ // Clearing the recoverable channel must not clear a fatal error.
115
+ hooks.onContentError!(null);
116
+ await nextTick();
117
+ expect(wrapper.text()).toContain("No resource content in response");
118
+ });
119
+ });
120
+
121
+ // Referenced so the session stub is not tree-shaken out of the mock factory.
122
+ void sessionSpy;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Vue counterpart of react-core's `MCPAppsActivityRendererSeed.test.tsx`.
3
+ *
4
+ * The session is self-driving: given a `messageId`, it subscribes to the agent's
5
+ * message store and pushes tool input/result to the widget itself. That store is
6
+ * authoritative only for activities that live in it - a host can render an
7
+ * activity from an EXTERNAL messages list (absent from `agent.messages`), whose
8
+ * props may never change after mount. For those, the adapter must seed the
9
+ * widget by calling `session.syncContent(props.content)` immediately after
10
+ * binding, otherwise the widget would never receive its initial content.
11
+ */
12
+ import { describe, expect, it, vi, beforeEach } from "vitest";
13
+ import { mount } from "@vue/test-utils";
14
+ import type { AbstractAgent } from "@ag-ui/client";
15
+
16
+ // Capture the bindMcpApp options and the returned session's calls. Hoisted so
17
+ // the vi.mock factory (which is itself hoisted above imports) can reference them.
18
+ const { bindMcpAppSpy, sessionSpy } = vi.hoisted(() => {
19
+ const session = {
20
+ sendToolInput: vi.fn(),
21
+ sendToolResult: vi.fn(),
22
+ syncContent: vi.fn(),
23
+ teardown: vi.fn(),
24
+ };
25
+ const bind = vi.fn(() => session);
26
+ return { bindMcpAppSpy: bind, sessionSpy: session };
27
+ });
28
+
29
+ // Only the bridge-carrying root entry is mocked; the bridge-free `/activity`
30
+ // entry (activity type + content schema) stays real, as in the React test.
31
+ vi.mock("@copilotkit/mcp-apps-renderer", () => ({
32
+ bindMcpApp: bindMcpAppSpy,
33
+ }));
34
+
35
+ vi.mock("../../providers/useCopilotKit", () => ({
36
+ useCopilotKit: () => ({
37
+ copilotkit: { value: { runAgent: vi.fn() } },
38
+ }),
39
+ }));
40
+
41
+ import {
42
+ MCPAppsActivityRenderer,
43
+ MCPAppsActivityType,
44
+ } from "../MCPAppsActivityRenderer";
45
+ import type { MCPAppsActivityContent } from "../MCPAppsActivityRenderer";
46
+
47
+ function createAgentMock(): AbstractAgent {
48
+ return {
49
+ threadId: "seed-thread",
50
+ isRunning: false,
51
+ // The activity under test is NOT in the store: this is the external
52
+ // messages-list case the seed exists for.
53
+ messages: [],
54
+ runAgent: vi.fn().mockResolvedValue({ result: { contents: [] } }),
55
+ addMessage: vi.fn(),
56
+ subscribe: vi.fn(() => ({ unsubscribe: vi.fn() })),
57
+ } as unknown as AbstractAgent;
58
+ }
59
+
60
+ const content: MCPAppsActivityContent = {
61
+ resourceUri: "ui://server/external",
62
+ serverHash: "hash-external",
63
+ toolInput: { city: "Paris" },
64
+ result: { content: [{ type: "text", text: "Forecast" }], isError: false },
65
+ } as MCPAppsActivityContent;
66
+
67
+ function mountRenderer() {
68
+ return mount(MCPAppsActivityRenderer, {
69
+ props: {
70
+ activityType: MCPAppsActivityType,
71
+ content,
72
+ message: {
73
+ id: "external-activity-1",
74
+ role: "activity",
75
+ content: {},
76
+ activityType: MCPAppsActivityType,
77
+ },
78
+ agent: createAgentMock(),
79
+ },
80
+ });
81
+ }
82
+
83
+ describe("MCPAppsActivityRenderer external-activity seed", () => {
84
+ beforeEach(() => {
85
+ vi.clearAllMocks();
86
+ });
87
+
88
+ it("passes the activity messageId to the bridge for self-subscription", async () => {
89
+ mountRenderer();
90
+
91
+ await vi.waitFor(() => expect(bindMcpAppSpy).toHaveBeenCalledTimes(1));
92
+ const opts = bindMcpAppSpy.mock.calls[0]![0];
93
+ expect(opts.messageId).toBe("external-activity-1");
94
+ });
95
+
96
+ it("seeds the widget with the initial tool input/result right after binding, even when props never change", async () => {
97
+ mountRenderer();
98
+
99
+ // The content watcher does not re-run for unchanged props, so without the
100
+ // explicit seed the widget would never receive this initial content.
101
+ await vi.waitFor(() => expect(sessionSpy.syncContent).toHaveBeenCalled());
102
+ expect(sessionSpy.syncContent).toHaveBeenCalledWith(
103
+ expect.objectContaining({
104
+ toolInput: { city: "Paris" },
105
+ resourceUri: "ui://server/external",
106
+ }),
107
+ );
108
+ });
109
+ });
@@ -352,7 +352,7 @@ onBeforeUnmount(() => {
352
352
  <div
353
353
  v-if="isRendered"
354
354
  data-copilotkit
355
- class="cpk:fixed cpk:inset-0 cpk:z-[1200] cpk:flex cpk:max-w-full cpk:flex-col cpk:items-stretch cpk:md:inset-auto cpk:md:bottom-24 cpk:md:right-6 cpk:md:items-end cpk:md:gap-4"
355
+ class="cpk:fixed cpk:inset-0 cpk:z-[1200] cpk:flex cpk:max-w-full cpk:flex-col cpk:items-stretch cpk:bg-transparent cpk:md:inset-auto cpk:md:bottom-24 cpk:md:right-6 cpk:md:items-end cpk:md:gap-4"
356
356
  >
357
357
  <div
358
358
  ref="containerRef"
@@ -540,6 +540,26 @@ describe("CopilotPopupView Slot System E2E Tests", () => {
540
540
  });
541
541
 
542
542
  describe("8. Integration Tests", () => {
543
+ it("keeps the transparent-background utility on the positioning wrapper", () => {
544
+ const Host = defineComponent({
545
+ components: { CopilotPopupView },
546
+ setup() {
547
+ return { sampleMessages };
548
+ },
549
+ template: `<CopilotPopupView :messages="sampleMessages" :default-open="true" />`,
550
+ });
551
+
552
+ const { container } = renderInWrapper(Host);
553
+ const popup = container.querySelector("[data-copilot-popup]");
554
+ const positioningWrapper = popup?.parentElement;
555
+
556
+ expect(positioningWrapper?.hasAttribute("data-copilotkit")).toBe(true);
557
+ // jsdom does not load the generated stylesheet; this guards the utility-class contract only.
558
+ expect(positioningWrapper?.classList.contains("cpk:bg-transparent")).toBe(
559
+ true,
560
+ );
561
+ });
562
+
543
563
  it("should render popup with all default components when open", () => {
544
564
  const Host = defineComponent({
545
565
  components: { CopilotPopupView },
package/vite.config.ts CHANGED
@@ -29,6 +29,13 @@ export default defineConfig({
29
29
  "@ag-ui/client",
30
30
  "@ag-ui/core",
31
31
  "@copilotkit/core",
32
+ // Keep the shared MCP Apps host + its subpaths external. The root entry
33
+ // carries the ext-apps AppBridge and is loaded through a dynamic
34
+ // import() in the renderer, so it must stay a real runtime import (not
35
+ // be inlined, which would pull the bridge into the eager chunk and undo
36
+ // the lazy-loading); `/activity` is the bridge-free surface imported
37
+ // statically for activity registration.
38
+ /^@copilotkit\/mcp-apps-renderer(\/.*)?$/,
32
39
  "@copilotkit/shared",
33
40
  "@copilotkit/web-inspector",
34
41
  // Keep @copilotkit/web-components (the Lit drawer element) + its subpaths