@copilotkit/vue 1.69.3 → 1.70.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 (32) hide show
  1. package/dist/hooks/use-copilot-action.d.ts +7 -0
  2. package/dist/hooks/use-copilot-action.d.ts.map +1 -1
  3. package/dist/hooks/use-frontend-tool.d.ts +7 -0
  4. package/dist/hooks/use-frontend-tool.d.ts.map +1 -1
  5. package/dist/index.cjs +1 -1
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.mjs +35 -23
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/use-render-activity-message-CyThomMH.cjs +85 -0
  10. package/dist/use-render-activity-message-CyThomMH.cjs.map +1 -0
  11. package/dist/{use-render-activity-message-W2lbqLZH.js → use-render-activity-message-WeN5iF5H.js} +2229 -2204
  12. package/dist/use-render-activity-message-WeN5iF5H.js.map +1 -0
  13. package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts +40 -16
  14. package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts.map +1 -1
  15. package/dist/v2/hooks/use-frontend-tool.d.ts.map +1 -1
  16. package/dist/v2/index.cjs +1 -1
  17. package/dist/v2/index.mjs +1 -1
  18. package/dist/v2/providers/CopilotKitProvider.vue.d.ts +2 -2
  19. package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
  20. package/package.json +7 -7
  21. package/src/hooks/__tests__/use-frontend-tool-webmcp.test.ts +113 -0
  22. package/src/hooks/use-copilot-action.ts +13 -0
  23. package/src/hooks/use-frontend-tool.ts +13 -0
  24. package/src/v2/hooks/__tests__/use-frontend-tool-webmcp.test.ts +282 -0
  25. package/src/v2/hooks/__tests__/use-threads.test.ts +24 -1
  26. package/src/v2/hooks/use-frontend-tool.ts +3 -0
  27. package/src/v2/hooks/use-threads.ts +1 -1
  28. package/src/v2/providers/CopilotKitProvider.vue +69 -14
  29. package/src/v2/providers/__tests__/CopilotKitProvider.managedEntitlement.test.ts +290 -0
  30. package/dist/use-render-activity-message-DMY6Krn-.cjs +0 -85
  31. package/dist/use-render-activity-message-DMY6Krn-.cjs.map +0 -1
  32. package/dist/use-render-activity-message-W2lbqLZH.js.map +0 -1
@@ -0,0 +1,282 @@
1
+ import { defineComponent, ref, watch } from "vue";
2
+ import { screen, fireEvent, waitFor } from "@testing-library/vue";
3
+ import { z } from "zod";
4
+ import { describe, it, expect } from "vitest";
5
+ import { useFrontendTool } from "../use-frontend-tool";
6
+ import { useCopilotKit } from "../../providers/useCopilotKit";
7
+ import type { VueFrontendTool } from "../../types";
8
+ import type { CopilotKitCoreVue } from "../../lib/vue-core";
9
+ import { renderWithCopilotKit } from "../../__tests__/utils/test-helpers";
10
+
11
+ /**
12
+ * Component that captures the copilotkit core ref for test assertions.
13
+ */
14
+ const CoreCapture = defineComponent({
15
+ props: {
16
+ onCore: {
17
+ type: Function as () => (core: CopilotKitCoreVue) => void,
18
+ required: true,
19
+ },
20
+ },
21
+ setup(props) {
22
+ const { copilotkit } = useCopilotKit();
23
+ watch(
24
+ copilotkit,
25
+ (core) => {
26
+ if (core) {
27
+ props.onCore(core);
28
+ }
29
+ },
30
+ { immediate: true },
31
+ );
32
+ return {};
33
+ },
34
+ template: `<div />`,
35
+ });
36
+
37
+ describe("useFrontendTool webmcp flag", () => {
38
+ it("registers tool with webmcp: true on the core", async () => {
39
+ let coreRef: CopilotKitCoreVue | null = null;
40
+
41
+ const ToolComponent = defineComponent({
42
+ setup() {
43
+ const tool: VueFrontendTool<{ msg: string }> = {
44
+ name: "webmcpTool",
45
+ description: "A WebMCP-exposed tool",
46
+ webmcp: true,
47
+ parameters: z.object({ msg: z.string() }),
48
+ handler: async () => ({ result: "ok" }),
49
+ };
50
+ useFrontendTool(tool);
51
+ return {};
52
+ },
53
+ template: `<div />`,
54
+ });
55
+
56
+ const Host = defineComponent({
57
+ components: { ToolComponent, CoreCapture },
58
+ setup() {
59
+ return {
60
+ setCore: (core: CopilotKitCoreVue) => {
61
+ coreRef = core;
62
+ },
63
+ };
64
+ },
65
+ template: `
66
+ <div>
67
+ <ToolComponent />
68
+ <CoreCapture :on-core="setCore" />
69
+ </div>
70
+ `,
71
+ });
72
+
73
+ const ui = renderWithCopilotKit({
74
+ children: Host,
75
+ });
76
+
77
+ await waitFor(() => {
78
+ expect(coreRef).not.toBeNull();
79
+ const tool = coreRef!.tools.find((entry) => entry.name === "webmcpTool");
80
+ expect(tool).toBeDefined();
81
+ expect(tool!.webmcp).toBe(true);
82
+ });
83
+
84
+ ui.unmount();
85
+ });
86
+
87
+ it("registers tool with webmcp annotations on the core", async () => {
88
+ let coreRef: CopilotKitCoreVue | null = null;
89
+
90
+ const ToolComponent = defineComponent({
91
+ setup() {
92
+ const tool: VueFrontendTool<{ msg: string }> = {
93
+ name: "annotatedTool",
94
+ description: "A WebMCP-exposed tool with annotations",
95
+ webmcp: { annotations: { readOnlyHint: true } },
96
+ parameters: z.object({ msg: z.string() }),
97
+ handler: async () => ({ result: "ok" }),
98
+ };
99
+ useFrontendTool(tool);
100
+ return {};
101
+ },
102
+ template: `<div />`,
103
+ });
104
+
105
+ const Host = defineComponent({
106
+ components: { ToolComponent, CoreCapture },
107
+ setup() {
108
+ return {
109
+ setCore: (core: CopilotKitCoreVue) => {
110
+ coreRef = core;
111
+ },
112
+ };
113
+ },
114
+ template: `
115
+ <div>
116
+ <ToolComponent />
117
+ <CoreCapture :on-core="setCore" />
118
+ </div>
119
+ `,
120
+ });
121
+
122
+ const ui = renderWithCopilotKit({
123
+ children: Host,
124
+ });
125
+
126
+ await waitFor(() => {
127
+ expect(coreRef).not.toBeNull();
128
+ const tool = coreRef!.tools.find(
129
+ (entry) => entry.name === "annotatedTool",
130
+ );
131
+ expect(tool).toBeDefined();
132
+ expect(tool!.webmcp).toEqual({ annotations: { readOnlyHint: true } });
133
+ });
134
+
135
+ ui.unmount();
136
+ });
137
+
138
+ it("re-registers tool when the webmcp config changes", async () => {
139
+ let coreRef: CopilotKitCoreVue | null = null;
140
+
141
+ const ToolWithToggle = defineComponent({
142
+ setup() {
143
+ const readOnly = ref(false);
144
+
145
+ const tool: VueFrontendTool<{ data: string }> = {
146
+ name: "toggleWebmcpTool",
147
+ description: "A tool with a toggleable WebMCP config",
148
+ get webmcp() {
149
+ return { annotations: { readOnlyHint: readOnly.value } };
150
+ },
151
+ parameters: z.object({ data: z.string() }),
152
+ handler: async () => ({ ok: true }),
153
+ };
154
+ useFrontendTool(tool, [readOnly]);
155
+
156
+ return {
157
+ readOnly,
158
+ toggle: () => {
159
+ readOnly.value = !readOnly.value;
160
+ },
161
+ };
162
+ },
163
+ template: `
164
+ <button data-testid="toggle-btn" @click="toggle">
165
+ {{ readOnly ? "Read-write" : "Read-only" }}
166
+ </button>
167
+ `,
168
+ });
169
+
170
+ const Host = defineComponent({
171
+ components: { ToolWithToggle, CoreCapture },
172
+ setup() {
173
+ return {
174
+ setCore: (core: CopilotKitCoreVue) => {
175
+ coreRef = core;
176
+ },
177
+ };
178
+ },
179
+ template: `
180
+ <div>
181
+ <ToolWithToggle />
182
+ <CoreCapture :on-core="setCore" />
183
+ </div>
184
+ `,
185
+ });
186
+
187
+ const ui = renderWithCopilotKit({
188
+ children: Host,
189
+ });
190
+
191
+ await waitFor(() => {
192
+ expect(coreRef).not.toBeNull();
193
+ const tool = coreRef!.tools.find(
194
+ (entry) => entry.name === "toggleWebmcpTool",
195
+ );
196
+ expect(tool).toBeDefined();
197
+ expect(tool!.webmcp).toEqual({ annotations: { readOnlyHint: false } });
198
+ });
199
+
200
+ await fireEvent.click(screen.getByTestId("toggle-btn"));
201
+
202
+ await waitFor(() => {
203
+ const tool = coreRef!.tools.find(
204
+ (entry) => entry.name === "toggleWebmcpTool",
205
+ );
206
+ expect(tool).toBeDefined();
207
+ expect(tool!.webmcp).toEqual({ annotations: { readOnlyHint: true } });
208
+ });
209
+
210
+ ui.unmount();
211
+ });
212
+
213
+ it("registers and cleans up agent-scoped webmcp tools on the core", async () => {
214
+ let coreRef: CopilotKitCoreVue | null = null;
215
+
216
+ const ToolComponent = defineComponent({
217
+ setup() {
218
+ useFrontendTool({
219
+ name: "scopedTool",
220
+ description: "A WebMCP tool scoped to alpha",
221
+ agentId: "alpha",
222
+ webmcp: { annotations: { readOnlyHint: true } },
223
+ handler: async () => ({ ok: true }),
224
+ });
225
+ useFrontendTool({
226
+ name: "scopedTool",
227
+ description: "A WebMCP tool scoped to beta",
228
+ agentId: "beta",
229
+ webmcp: true,
230
+ handler: async () => ({ ok: true }),
231
+ });
232
+ return {};
233
+ },
234
+ template: `<div />`,
235
+ });
236
+
237
+ const Host = defineComponent({
238
+ components: { ToolComponent, CoreCapture },
239
+ setup() {
240
+ return {
241
+ setCore: (core: CopilotKitCoreVue) => {
242
+ coreRef = core;
243
+ },
244
+ };
245
+ },
246
+ template: `
247
+ <div>
248
+ <ToolComponent />
249
+ <CoreCapture :on-core="setCore" />
250
+ </div>
251
+ `,
252
+ });
253
+
254
+ const ui = renderWithCopilotKit({
255
+ children: Host,
256
+ });
257
+
258
+ await waitFor(() => {
259
+ expect(coreRef).not.toBeNull();
260
+ const alphaTool = coreRef!.tools.find(
261
+ (entry) => entry.name === "scopedTool" && entry.agentId === "alpha",
262
+ );
263
+ expect(alphaTool).toBeDefined();
264
+ expect(alphaTool!.webmcp).toEqual({
265
+ annotations: { readOnlyHint: true },
266
+ });
267
+ const betaTool = coreRef!.tools.find(
268
+ (entry) => entry.name === "scopedTool" && entry.agentId === "beta",
269
+ );
270
+ expect(betaTool).toBeDefined();
271
+ expect(betaTool!.webmcp).toBe(true);
272
+ });
273
+
274
+ ui.unmount();
275
+
276
+ await waitFor(() => {
277
+ expect(
278
+ coreRef!.tools.filter((entry) => entry.name === "scopedTool").length,
279
+ ).toBe(0);
280
+ });
281
+ });
282
+ });
@@ -44,6 +44,8 @@ const mockUseCopilotKit = useCopilotKit as ReturnType<typeof vi.fn>;
44
44
  const threadMocks = vi.hoisted(() => ({
45
45
  sockets: [] as any[],
46
46
  dispatchedContexts: [] as Array<ThreadState["context"]>,
47
+ /** Every environment `ɵcreateThreadStore` was constructed with. */
48
+ storeEnvironments: [] as Array<{ fetch?: unknown }>,
47
49
  }));
48
50
 
49
51
  function compareThreadsByActivity(left: ThreadRecord, right: ThreadRecord) {
@@ -344,7 +346,10 @@ vi.mock("@copilotkit/core", () => {
344
346
  Connected: "connected",
345
347
  Error: "error",
346
348
  },
347
- ɵcreateThreadStore: () => new MockThreadStore(),
349
+ ɵcreateThreadStore: (environment: { fetch?: unknown }) => {
350
+ threadMocks.storeEnvironments.push(environment);
351
+ return new MockThreadStore();
352
+ },
348
353
  ɵselectThreads: select((state) => state.threads),
349
354
  ɵselectThreadsIsLoading: select((state) => state.isLoading),
350
355
  ɵselectThreadsError: select((state) => state.error),
@@ -358,6 +363,10 @@ vi.mock("@copilotkit/core", () => {
358
363
  const fetchMock = vi.fn();
359
364
  globalThis.fetch = fetchMock;
360
365
 
366
+ const runtimeFetchMock = vi.fn((...args: unknown[]) =>
367
+ (fetchMock as (...a: unknown[]) => unknown)(...args),
368
+ );
369
+
361
370
  const { CopilotKitCoreRuntimeConnectionStatus } =
362
371
  await import("@copilotkit/core");
363
372
 
@@ -391,6 +400,7 @@ type MockCopilotKit = {
391
400
  threadEndpoints: ThreadEndpointRuntimeInfo | undefined;
392
401
  registerThreadStore: ReturnType<typeof vi.fn>;
393
402
  unregisterThreadStore: ReturnType<typeof vi.fn>;
403
+ ɵruntimeFetch: typeof fetch;
394
404
  };
395
405
 
396
406
  function setupCopilotKit(
@@ -410,6 +420,7 @@ function setupCopilotKit(
410
420
  threadEndpoints: supportedThreadEndpoints,
411
421
  registerThreadStore: vi.fn(),
412
422
  unregisterThreadStore: vi.fn(),
423
+ ɵruntimeFetch: runtimeFetchMock as unknown as typeof fetch,
413
424
  ...overrides,
414
425
  });
415
426
  mockUseCopilotKit.mockReturnValue({ copilotkit });
@@ -487,10 +498,22 @@ describe("useThreads", () => {
487
498
  beforeEach(() => {
488
499
  threadMocks.sockets.splice(0);
489
500
  threadMocks.dispatchedContexts.splice(0);
501
+ threadMocks.storeEnvironments.splice(0);
490
502
  fetchMock.mockReset();
491
503
  setupCopilotKit();
492
504
  });
493
505
 
506
+ it("builds the thread store with the core's instrumented fetch", () => {
507
+ const copilotkit = setupCopilotKit();
508
+ mountHook();
509
+
510
+ expect(threadMocks.storeEnvironments).toHaveLength(1);
511
+ expect(threadMocks.storeEnvironments[0]!.fetch).toBe(
512
+ copilotkit.value.ɵruntimeFetch,
513
+ );
514
+ expect(threadMocks.storeEnvironments[0]!.fetch).not.toBe(globalThis.fetch);
515
+ });
516
+
494
517
  it("fetches threads and subscribes to the user metadata channel", async () => {
495
518
  fetchMock
496
519
  .mockReturnValueOnce(
@@ -32,6 +32,9 @@ export function useFrontendTool<T extends Record<string, unknown>>(
32
32
  [
33
33
  () => tool.name,
34
34
  () => tool.available,
35
+ // webmcp is commonly an object literal, so compare it by value to avoid
36
+ // re-registering on identity churn while still reacting to real changes.
37
+ () => JSON.stringify(tool.webmcp ?? null),
35
38
  () => extraDeps.length,
36
39
  ...extraDeps,
37
40
  ],
@@ -90,7 +90,7 @@ function bindThreadStoreSelector<T>(
90
90
  export function useThreads(input: UseThreadsInput): UseThreadsResult {
91
91
  const { copilotkit } = useCopilotKit();
92
92
  const store = ɵcreateThreadStore({
93
- fetch: globalThis.fetch,
93
+ fetch: copilotkit.value.ɵruntimeFetch,
94
94
  });
95
95
 
96
96
  const resolvedAgentId = computed(() => toValue(input.agentId));
@@ -16,7 +16,10 @@ import type {
16
16
  FrontendTool,
17
17
  } from "@copilotkit/core";
18
18
  import { schemaToJsonSchema, shouldEnableInspector } from "@copilotkit/shared";
19
- import type { RuntimeLicenseStatus } from "@copilotkit/shared";
19
+ import type {
20
+ RuntimeEntitlementResponse,
21
+ RuntimeLicenseStatus,
22
+ } from "@copilotkit/shared";
20
23
  import { zodToJsonSchema } from "zod-to-json-schema";
21
24
  import { CopilotKitCoreVue } from "../lib/vue-core";
22
25
  import { createA2UIMessageRenderer } from "../components/A2UIMessageRenderer";
@@ -48,7 +51,6 @@ import type {
48
51
  SandboxFunction,
49
52
  VueActivityMessageRenderer,
50
53
  VueFrontendTool,
51
- VueHumanInTheLoop,
52
54
  VueToolCallRenderer,
53
55
  } from "../types";
54
56
 
@@ -294,6 +296,10 @@ const allRenderCustomMessages = computed(
294
296
  const runtimeA2UIEnabled = ref(false);
295
297
  const runtimeOpenGenerativeUIEnabled = ref(false);
296
298
  const runtimeLicenseStatus = ref<RuntimeLicenseStatus | undefined>(undefined);
299
+ const runtimeEntitlements = ref<RuntimeEntitlementResponse | undefined>(
300
+ undefined,
301
+ );
302
+ const runtimeEntitlementRetryPending = ref(false);
297
303
  const openGenerativeUIActive = computed(
298
304
  () => runtimeOpenGenerativeUIEnabled.value || !!props.openGenerativeUI,
299
305
  );
@@ -460,15 +466,14 @@ watch(
460
466
  runtimeA2UIEnabled.value = core.a2uiEnabled;
461
467
  runtimeOpenGenerativeUIEnabled.value = core.openGenerativeUIEnabled;
462
468
  runtimeLicenseStatus.value = core.licenseStatus;
469
+ runtimeEntitlements.value = core.runtimeEntitlements;
470
+ runtimeEntitlementRetryPending.value =
471
+ core.runtimeEntitlementRetryPending;
463
472
  triggerRef(copilotkit);
464
473
  },
465
474
  });
466
475
  const sub5 = core.subscribe({
467
- onError: (event: {
468
- error: Error;
469
- code: CopilotKitCoreErrorCode;
470
- context: Record<string, any>;
471
- }) => {
476
+ onError: (event) => {
472
477
  void props.onError?.({
473
478
  error: event.error,
474
479
  code: event.code,
@@ -557,6 +562,9 @@ onMounted(() => {
557
562
  runtimeOpenGenerativeUIEnabled.value =
558
563
  copilotkit.value.openGenerativeUIEnabled;
559
564
  runtimeLicenseStatus.value = copilotkit.value.licenseStatus;
565
+ runtimeEntitlements.value = copilotkit.value.runtimeEntitlements;
566
+ runtimeEntitlementRetryPending.value =
567
+ copilotkit.value.runtimeEntitlementRetryPending;
560
568
  didMountRef.value = true;
561
569
  });
562
570
 
@@ -633,23 +641,70 @@ provide(CopilotKitKey, {
633
641
  });
634
642
  provide(SandboxFunctionsKey, sandboxFunctions);
635
643
 
636
- // License context — driven by server-reported `/info` license status.
637
- const licenseContextValue = computed<LicenseContextValue>(() =>
638
- createLicenseContextValue(runtimeLicenseStatus.value),
644
+ // License context — driven by structured and legacy Runtime authority.
645
+ const retryableRuntimeEntitlementFailure = computed(
646
+ () =>
647
+ runtimeEntitlements.value?.status !== "ready" &&
648
+ runtimeEntitlements.value?.error.retryable === true,
649
+ );
650
+ const hasNonReadyRuntimeEntitlement = computed(
651
+ () =>
652
+ runtimeEntitlements.value !== undefined &&
653
+ runtimeEntitlements.value.status !== "ready",
654
+ );
655
+ const hasLegacyRuntimeEntitlementFallback = computed(
656
+ () =>
657
+ runtimeLicenseStatus.value === "valid" ||
658
+ runtimeLicenseStatus.value === "expiring",
659
+ );
660
+ const runtimeEntitlementRetryInProgress = computed(
661
+ () =>
662
+ retryableRuntimeEntitlementFailure.value &&
663
+ runtimeEntitlementRetryPending.value &&
664
+ !hasLegacyRuntimeEntitlementFallback.value,
639
665
  );
666
+ const runtimeEntitlementFailureSettled = computed(
667
+ () =>
668
+ hasNonReadyRuntimeEntitlement.value &&
669
+ !runtimeEntitlementRetryInProgress.value &&
670
+ !hasLegacyRuntimeEntitlementFallback.value,
671
+ );
672
+ const licenseContextValue = computed<LicenseContextValue>(() => {
673
+ const runtimeLicenseContext = createLicenseContextValue(
674
+ runtimeEntitlementRetryInProgress.value
675
+ ? undefined
676
+ : runtimeLicenseStatus.value,
677
+ runtimeEntitlements.value,
678
+ );
679
+ if (!runtimeEntitlementFailureSettled.value) {
680
+ return runtimeLicenseContext;
681
+ }
682
+
683
+ return {
684
+ ...runtimeLicenseContext,
685
+ checkFeature: () => false,
686
+ getLimit: () => null,
687
+ };
688
+ });
640
689
  provide(LicenseContextKey, licenseContextValue);
641
690
 
691
+ const runtimeLicenseWarningStatus = computed(() =>
692
+ runtimeEntitlementRetryInProgress.value
693
+ ? undefined
694
+ : (licenseContextValue.value.status ?? undefined),
695
+ );
642
696
  const showNoLicenseBanner = computed(
643
- () => runtimeLicenseStatus.value === "none" && !resolvedPublicKey.value,
697
+ () =>
698
+ runtimeLicenseWarningStatus.value === "none" && !resolvedPublicKey.value,
644
699
  );
645
700
  const showExpiredBanner = computed(
646
- () => runtimeLicenseStatus.value === "expired",
701
+ () => runtimeLicenseWarningStatus.value === "expired",
647
702
  );
648
703
  const showInvalidBanner = computed(
649
- () => runtimeLicenseStatus.value === "invalid",
704
+ () => runtimeLicenseWarningStatus.value === "invalid",
650
705
  );
651
706
  const showExpiringBanner = computed(
652
- () => runtimeLicenseStatus.value === "expiring",
707
+ () => runtimeLicenseWarningStatus.value === "expiring",
653
708
  );
654
709
  </script>
655
710