@copilotkit/vue 1.62.1 → 1.62.2

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 (49) hide show
  1. package/dist/index-By5nIKxT.js +1632 -0
  2. package/dist/index-By5nIKxT.js.map +1 -0
  3. package/dist/index-LFxZI63k.cjs +748 -0
  4. package/dist/index-LFxZI63k.cjs.map +1 -0
  5. package/dist/index.cjs +1 -1
  6. package/dist/index.mjs +70 -69
  7. package/dist/styles.css +1 -1
  8. package/dist/{use-render-activity-message-8_HyVZWc.js → use-render-activity-message-DbGuwvBd.js} +3361 -3064
  9. package/dist/use-render-activity-message-DbGuwvBd.js.map +1 -0
  10. package/dist/use-render-activity-message-hlNcbV9l.cjs +85 -0
  11. package/dist/use-render-activity-message-hlNcbV9l.cjs.map +1 -0
  12. package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
  13. package/dist/v2/components/chat/CopilotModalHeader.vue.d.ts.map +1 -1
  14. package/dist/v2/components/chat/CopilotThreadsDrawer.vue.d.ts +42 -0
  15. package/dist/v2/components/chat/CopilotThreadsDrawer.vue.d.ts.map +1 -0
  16. package/dist/v2/components/chat/index.d.ts +1 -0
  17. package/dist/v2/components/chat/index.d.ts.map +1 -1
  18. package/dist/v2/components/icons/index.d.ts +1 -1
  19. package/dist/v2/components/icons/index.d.ts.map +1 -1
  20. package/dist/v2/hooks/use-threads.d.ts +12 -0
  21. package/dist/v2/hooks/use-threads.d.ts.map +1 -1
  22. package/dist/v2/index.cjs +1 -1
  23. package/dist/v2/index.mjs +44 -43
  24. package/dist/v2/lib/is-mobile-viewport.d.ts +15 -0
  25. package/dist/v2/lib/is-mobile-viewport.d.ts.map +1 -0
  26. package/dist/v2/providers/CopilotChatConfigurationProvider.vue.d.ts.map +1 -1
  27. package/dist/v2/providers/types.d.ts +24 -0
  28. package/dist/v2/providers/types.d.ts.map +1 -1
  29. package/package.json +5 -4
  30. package/src/v2/components/chat/CopilotChat.vue +22 -4
  31. package/src/v2/components/chat/CopilotModalHeader.vue +61 -2
  32. package/src/v2/components/chat/CopilotThreadsDrawer.vue +285 -0
  33. package/src/v2/components/chat/__tests__/CopilotChat.clearOnFresh.test.ts +160 -0
  34. package/src/v2/components/chat/__tests__/CopilotModalHeader.drawerLauncher.test.ts +125 -0
  35. package/src/v2/components/chat/__tests__/CopilotThreadsDrawer.ssr.test.ts +25 -0
  36. package/src/v2/components/chat/__tests__/CopilotThreadsDrawer.test.ts +835 -0
  37. package/src/v2/components/chat/index.ts +1 -0
  38. package/src/v2/components/icons/index.ts +1 -0
  39. package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.ts +4 -4
  40. package/src/v2/hooks/__tests__/use-threads.test.ts +303 -6
  41. package/src/v2/hooks/use-threads.ts +137 -8
  42. package/src/v2/lib/__tests__/is-mobile-viewport.test.ts +48 -0
  43. package/src/v2/lib/is-mobile-viewport.ts +23 -0
  44. package/src/v2/providers/CopilotChatConfigurationProvider.vue +120 -4
  45. package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.ts +105 -1
  46. package/src/v2/providers/types.ts +25 -0
  47. package/dist/use-render-activity-message-8_HyVZWc.js.map +0 -1
  48. package/dist/use-render-activity-message-BqgmNPCz.cjs +0 -85
  49. package/dist/use-render-activity-message-BqgmNPCz.cjs.map +0 -1
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Media query for the mobile breakpoint, shared so the string literal isn't
3
+ * duplicated across call sites within this package (e.g. `CopilotModalHeader`,
4
+ * which needs a live `matchMedia` change-listener and so can't call
5
+ * `isMobileViewport()` directly).
6
+ */
7
+ export const MOBILE_MAX_WIDTH_QUERY = "(max-width: 767px)";
8
+
9
+ /**
10
+ * Reports whether the viewport is in the mobile range (`<768px`), where the
11
+ * drawer and chat modal are mutually exclusive. SSR-safe (returns `false`
12
+ * when `window` is absent) and guards environments where `window` exists but
13
+ * `window.matchMedia` does not (some test runners, embedded webviews).
14
+ */
15
+ export function isMobileViewport(): boolean {
16
+ if (
17
+ typeof window === "undefined" ||
18
+ typeof window.matchMedia !== "function"
19
+ ) {
20
+ return false;
21
+ }
22
+ return window.matchMedia(MOBILE_MAX_WIDTH_QUERY).matches;
23
+ }
@@ -7,6 +7,7 @@ import { CopilotChatDefaultLabels } from "./types";
7
7
  import type { CopilotChatConfigurationValue, CopilotChatLabels } from "./types";
8
8
  import type { CopilotChatConfigurationProviderProps } from "./CopilotChatConfigurationProvider.types";
9
9
  import { useShallowStableRef } from "../lib/shallow-stable";
10
+ import { isMobileViewport } from "../lib/is-mobile-viewport";
10
11
 
11
12
  // Vue normalizes optional Boolean props to `false` when not supplied; declare
12
13
  // `undefined` defaults so we can faithfully distinguish "caller passed false"
@@ -24,6 +25,20 @@ const parentConfig = inject<ComputedRef<CopilotChatConfigurationValue> | null>(
24
25
  null,
25
26
  );
26
27
  const parentConfigValue = computed(() => parentConfig?.value ?? null);
28
+
29
+ // Caller-authoritative when a threadId prop is supplied AND not explicitly
30
+ // flagged non-explicit. A <CopilotKit>-seeded non-explicit id
31
+ // (threadId + hasExplicitThreadId=false) stays overridable — the round-2 fix.
32
+ const propIsAuthoritative = computed(
33
+ () => props.threadId !== undefined && props.hasExplicitThreadId !== false,
34
+ );
35
+
36
+ // Imperative active-thread override (a picked row or a fresh startNewThread).
37
+ const activeThreadOverride = ref<{
38
+ threadId: string;
39
+ explicit: boolean;
40
+ } | null>(null);
41
+
27
42
  const stableLabels = useShallowStableRef(computed(() => props.labels));
28
43
 
29
44
  const mergedLabels = computed<CopilotChatLabels>(() => ({
@@ -38,6 +53,8 @@ const resolvedAgentId = computed(
38
53
 
39
54
  const fallbackThreadId = randomUUID();
40
55
  const resolvedThreadId = computed(() => {
56
+ if (propIsAuthoritative.value) return props.threadId as string;
57
+ if (activeThreadOverride.value) return activeThreadOverride.value.threadId;
41
58
  if (props.threadId) return props.threadId;
42
59
  if (parentConfigValue.value?.threadId)
43
60
  return parentConfigValue.value.threadId;
@@ -45,11 +62,13 @@ const resolvedThreadId = computed(() => {
45
62
  });
46
63
 
47
64
  const resolvedHasExplicitThreadId = computed(() => {
48
- const ownExplicit =
49
- props.hasExplicitThreadId !== undefined
65
+ if (propIsAuthoritative.value) return true;
66
+ const own = activeThreadOverride.value
67
+ ? activeThreadOverride.value.explicit
68
+ : props.hasExplicitThreadId !== undefined
50
69
  ? props.hasExplicitThreadId
51
70
  : !!props.threadId;
52
- return ownExplicit || !!parentConfigValue.value?.hasExplicitThreadId;
71
+ return own || !!parentConfigValue.value?.hasExplicitThreadId;
53
72
  });
54
73
 
55
74
  const shouldCreateModalState = computed(
@@ -76,13 +95,110 @@ const resolvedSetModalOpen = computed(() =>
76
95
  : parentConfigValue.value?.setModalOpen,
77
96
  );
78
97
 
98
+ function setActiveThreadId(threadId: string, options?: { explicit?: boolean }) {
99
+ if (propIsAuthoritative.value) {
100
+ console.warn(
101
+ "[CopilotKit] Ignoring setActiveThreadId(): threadId is controlled " +
102
+ "via the `threadId` prop on CopilotChatConfigurationProvider.",
103
+ );
104
+ return;
105
+ }
106
+ activeThreadOverride.value = {
107
+ threadId,
108
+ explicit: options?.explicit ?? true,
109
+ };
110
+ }
111
+
112
+ function startNewThread() {
113
+ if (propIsAuthoritative.value) {
114
+ console.warn(
115
+ "[CopilotKit] Ignoring startNewThread(): threadId is controlled via " +
116
+ "the `threadId` prop on CopilotChatConfigurationProvider.",
117
+ );
118
+ return;
119
+ }
120
+ activeThreadOverride.value = { threadId: randomUUID(), explicit: false };
121
+ }
122
+
123
+ // Drawer presence + open state. The top-most provider owns the state; a nested
124
+ // provider proxies its parent so the whole chain shares one drawer.
125
+ const ownDrawerOpen = ref(false);
126
+ const ownDrawerCount = ref(0);
127
+
128
+ function ownSetDrawerOpen(open: boolean) {
129
+ ownDrawerOpen.value = open;
130
+ // Mobile mutual-exclusion: opening the drawer closes the chat modal.
131
+ if (open && isMobileViewport()) {
132
+ resolvedSetModalOpen.value?.(false);
133
+ }
134
+ }
135
+
136
+ function ownRegisterDrawer(): () => void {
137
+ ownDrawerCount.value += 1;
138
+ return () => {
139
+ ownDrawerCount.value = Math.max(0, ownDrawerCount.value - 1);
140
+ };
141
+ }
142
+
143
+ const resolvedDrawerOpen = computed(() =>
144
+ parentConfigValue.value
145
+ ? parentConfigValue.value.drawerOpen
146
+ : ownDrawerOpen.value,
147
+ );
148
+ const resolvedSetDrawerOpen = computed(() =>
149
+ parentConfigValue.value
150
+ ? parentConfigValue.value.setDrawerOpen
151
+ : ownSetDrawerOpen,
152
+ );
153
+ const resolvedDrawerRegistered = computed(() =>
154
+ parentConfigValue.value
155
+ ? parentConfigValue.value.drawerRegistered
156
+ : ownDrawerCount.value > 0,
157
+ );
158
+ const resolvedRegisterDrawer = computed(() =>
159
+ parentConfigValue.value
160
+ ? parentConfigValue.value.registerDrawer
161
+ : ownRegisterDrawer,
162
+ );
163
+
164
+ // Public modal setter (mobile mutual-exclusion, other direction: opening the
165
+ // chat modal closes the drawer). Preserve the contract that it is `undefined`
166
+ // when this provider owns no modal state AND no parent provides one —
167
+ // CopilotChatToggleButton relies on that absence to fall back to its own
168
+ // local open-state. When a real modal setter exists (own `isModalDefaultOpen`
169
+ // state or an inherited parent setter), wrap it so opening the modal on
170
+ // mobile also closes the drawer.
171
+ //
172
+ // Note: this diverges from the React reference (CopilotChatConfigurationProvider.tsx),
173
+ // which keeps `setModalOpen` always-defined and always backed by internal state. We
174
+ // intentionally take the minimal Vue-local fix here — preserving the `undefined`
175
+ // contract CopilotChatToggleButton depends on — rather than also reworking
176
+ // `resolvedIsModalOpen`'s backing, to avoid changing bare-provider modal-open
177
+ // semantics.
178
+ const publicSetModalOpen = computed(() => {
179
+ const base = resolvedSetModalOpen.value; // undefined when no modal state + no parent
180
+ if (!base) return undefined;
181
+ return (open: boolean) => {
182
+ if (open && isMobileViewport()) {
183
+ resolvedSetDrawerOpen.value?.(false);
184
+ }
185
+ base(open);
186
+ };
187
+ });
188
+
79
189
  const configurationValue = computed<CopilotChatConfigurationValue>(() => ({
80
190
  labels: mergedLabels.value,
81
191
  agentId: resolvedAgentId.value,
82
192
  threadId: resolvedThreadId.value,
83
193
  hasExplicitThreadId: resolvedHasExplicitThreadId.value,
84
194
  isModalOpen: resolvedIsModalOpen.value,
85
- setModalOpen: resolvedSetModalOpen.value,
195
+ setModalOpen: publicSetModalOpen.value,
196
+ drawerOpen: resolvedDrawerOpen.value,
197
+ setDrawerOpen: resolvedSetDrawerOpen.value,
198
+ drawerRegistered: resolvedDrawerRegistered.value,
199
+ registerDrawer: resolvedRegisterDrawer.value,
200
+ setActiveThreadId,
201
+ startNewThread,
86
202
  }));
87
203
 
88
204
  provide(CopilotChatConfigurationKey, configurationValue);
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { describe, expect, it, vi } from "vitest";
2
2
  import { mount } from "@vue/test-utils";
3
3
  import { defineComponent, h, nextTick } from "vue";
4
4
  import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
@@ -307,3 +307,107 @@ describe("CopilotChatConfigurationProvider", () => {
307
307
  expect(wrapper.find("[data-testid=child-modal]").text()).toBe("false");
308
308
  });
309
309
  });
310
+
311
+ function harness(providerProps: Record<string, unknown>) {
312
+ let cfg!: ReturnType<typeof useCopilotChatConfiguration>;
313
+ const Probe = defineComponent({
314
+ setup() {
315
+ cfg = useCopilotChatConfiguration();
316
+ return () => h("div", cfg.value?.threadId ?? "none");
317
+ },
318
+ });
319
+ mount(CopilotChatConfigurationProvider, {
320
+ props: providerProps,
321
+ slots: { default: () => h(Probe) },
322
+ });
323
+ return () => cfg.value!;
324
+ }
325
+
326
+ describe("CopilotChatConfiguration active-thread setters", () => {
327
+ it("setActiveThreadId overrides a non-explicit seed and flags it explicit", async () => {
328
+ // A <CopilotKit>-style non-explicit seed: threadId prop + hasExplicitThreadId=false.
329
+ const cfg = harness({ threadId: "seed-uuid", hasExplicitThreadId: false });
330
+ cfg().setActiveThreadId("picked-thread");
331
+ await nextTick();
332
+ expect(cfg().threadId).toBe("picked-thread");
333
+ expect(cfg().hasExplicitThreadId).toBe(true);
334
+ });
335
+
336
+ it("startNewThread mints a fresh non-explicit thread", async () => {
337
+ const cfg = harness({ threadId: "seed-uuid", hasExplicitThreadId: false });
338
+ cfg().setActiveThreadId("picked-thread");
339
+ await nextTick();
340
+ cfg().startNewThread();
341
+ await nextTick();
342
+ expect(cfg().threadId).not.toBe("picked-thread");
343
+ expect(cfg().hasExplicitThreadId).toBe(false);
344
+ });
345
+
346
+ it("a caller-authoritative threadId prop is NOT overridable", async () => {
347
+ const cfg = harness({ threadId: "controlled" }); // no hasExplicitThreadId => authoritative
348
+ cfg().setActiveThreadId("ignored");
349
+ await nextTick();
350
+ expect(cfg().threadId).toBe("controlled");
351
+ });
352
+ });
353
+
354
+ describe("CopilotChatConfiguration drawer-awareness", () => {
355
+ it("registerDrawer flips drawerRegistered and cleans up", async () => {
356
+ const cfg = harness({ isModalDefaultOpen: true });
357
+ expect(cfg().drawerRegistered).toBe(false);
358
+ const unregister = cfg().registerDrawer();
359
+ await nextTick();
360
+ expect(cfg().drawerRegistered).toBe(true);
361
+ unregister();
362
+ await nextTick();
363
+ expect(cfg().drawerRegistered).toBe(false);
364
+ });
365
+
366
+ it("opening the drawer on mobile closes the modal", async () => {
367
+ vi.spyOn(window, "matchMedia").mockReturnValue({
368
+ matches: true,
369
+ } as MediaQueryList);
370
+ const cfg = harness({ isModalDefaultOpen: true });
371
+ expect(cfg().isModalOpen).toBe(true);
372
+ cfg().setDrawerOpen(true);
373
+ await nextTick();
374
+ expect(cfg().drawerOpen).toBe(true);
375
+ expect(cfg().isModalOpen).toBe(false);
376
+ vi.restoreAllMocks();
377
+ });
378
+
379
+ it("opening the modal on mobile closes the drawer", async () => {
380
+ vi.spyOn(window, "matchMedia").mockReturnValue({
381
+ matches: true,
382
+ } as MediaQueryList);
383
+ const cfg = harness({ isModalDefaultOpen: true });
384
+
385
+ // First open the drawer (closing the modal via the existing exclusion).
386
+ cfg().setDrawerOpen(true);
387
+ await nextTick();
388
+ expect(cfg().drawerOpen).toBe(true);
389
+ expect(cfg().isModalOpen).toBe(false);
390
+
391
+ // Now open the modal and confirm the reverse exclusion closes the drawer.
392
+ cfg().setModalOpen(true);
393
+ await nextTick();
394
+ expect(cfg().isModalOpen).toBe(true);
395
+ expect(cfg().drawerOpen).toBe(false);
396
+ vi.restoreAllMocks();
397
+ });
398
+ });
399
+
400
+ describe("CopilotChatConfiguration modal-setter presence contract", () => {
401
+ it("bare provider (no isModalDefaultOpen, no parent) exposes setModalOpen as undefined", () => {
402
+ const cfg = harness({ threadId: "thread-1" });
403
+ expect(cfg().setModalOpen).toBeUndefined();
404
+ });
405
+
406
+ it("provider with isModalDefaultOpen exposes a working setModalOpen", async () => {
407
+ const cfg = harness({ isModalDefaultOpen: true });
408
+ expect(typeof cfg().setModalOpen).toBe("function");
409
+ cfg().setModalOpen(false);
410
+ await nextTick();
411
+ expect(cfg().isModalOpen).toBe(false);
412
+ });
413
+ });
@@ -37,4 +37,29 @@ export interface CopilotChatConfigurationValue {
37
37
  hasExplicitThreadId: boolean;
38
38
  isModalOpen?: boolean;
39
39
  setModalOpen?: (open: boolean) => void;
40
+ /**
41
+ * Switches the active thread to `threadId`. `explicit` (default `true`)
42
+ * marks it a caller-driven choice so the welcome screen is suppressed.
43
+ * No-op when the provider's `threadId` prop is caller-authoritative.
44
+ */
45
+ setActiveThreadId: (
46
+ threadId: string,
47
+ options?: { explicit?: boolean },
48
+ ) => void;
49
+ /**
50
+ * Switches to a freshly-minted, non-explicit thread (welcome screen shows).
51
+ * No-op when the provider's `threadId` prop is caller-authoritative.
52
+ */
53
+ startNewThread: () => void;
54
+ /** Whether the mobile off-canvas drawer overlay is open. */
55
+ drawerOpen: boolean;
56
+ /**
57
+ * Toggles the drawer. On mobile (`<=767px`) opening the drawer closes the
58
+ * chat modal (mutual-exclusion). Desktop imposes no constraint.
59
+ */
60
+ setDrawerOpen: (open: boolean) => void;
61
+ /** True when at least one drawer has registered (gates the header launcher). */
62
+ drawerRegistered: boolean;
63
+ /** Announces drawer presence; returns a cleanup that de-registers it. */
64
+ registerDrawer: () => () => void;
40
65
  }