@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.
- package/dist/index-By5nIKxT.js +1632 -0
- package/dist/index-By5nIKxT.js.map +1 -0
- package/dist/index-LFxZI63k.cjs +748 -0
- package/dist/index-LFxZI63k.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +70 -69
- package/dist/styles.css +1 -1
- package/dist/{use-render-activity-message-8_HyVZWc.js → use-render-activity-message-DbGuwvBd.js} +3361 -3064
- package/dist/use-render-activity-message-DbGuwvBd.js.map +1 -0
- package/dist/use-render-activity-message-hlNcbV9l.cjs +85 -0
- package/dist/use-render-activity-message-hlNcbV9l.cjs.map +1 -0
- package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotModalHeader.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotThreadsDrawer.vue.d.ts +42 -0
- package/dist/v2/components/chat/CopilotThreadsDrawer.vue.d.ts.map +1 -0
- package/dist/v2/components/chat/index.d.ts +1 -0
- package/dist/v2/components/chat/index.d.ts.map +1 -1
- package/dist/v2/components/icons/index.d.ts +1 -1
- package/dist/v2/components/icons/index.d.ts.map +1 -1
- package/dist/v2/hooks/use-threads.d.ts +12 -0
- package/dist/v2/hooks/use-threads.d.ts.map +1 -1
- package/dist/v2/index.cjs +1 -1
- package/dist/v2/index.mjs +44 -43
- package/dist/v2/lib/is-mobile-viewport.d.ts +15 -0
- package/dist/v2/lib/is-mobile-viewport.d.ts.map +1 -0
- package/dist/v2/providers/CopilotChatConfigurationProvider.vue.d.ts.map +1 -1
- package/dist/v2/providers/types.d.ts +24 -0
- package/dist/v2/providers/types.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/v2/components/chat/CopilotChat.vue +22 -4
- package/src/v2/components/chat/CopilotModalHeader.vue +61 -2
- package/src/v2/components/chat/CopilotThreadsDrawer.vue +285 -0
- package/src/v2/components/chat/__tests__/CopilotChat.clearOnFresh.test.ts +160 -0
- package/src/v2/components/chat/__tests__/CopilotModalHeader.drawerLauncher.test.ts +125 -0
- package/src/v2/components/chat/__tests__/CopilotThreadsDrawer.ssr.test.ts +25 -0
- package/src/v2/components/chat/__tests__/CopilotThreadsDrawer.test.ts +835 -0
- package/src/v2/components/chat/index.ts +1 -0
- package/src/v2/components/icons/index.ts +1 -0
- package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.ts +4 -4
- package/src/v2/hooks/__tests__/use-threads.test.ts +303 -6
- package/src/v2/hooks/use-threads.ts +137 -8
- package/src/v2/lib/__tests__/is-mobile-viewport.test.ts +48 -0
- package/src/v2/lib/is-mobile-viewport.ts +23 -0
- package/src/v2/providers/CopilotChatConfigurationProvider.vue +120 -4
- package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.ts +105 -1
- package/src/v2/providers/types.ts +25 -0
- package/dist/use-render-activity-message-8_HyVZWc.js.map +0 -1
- package/dist/use-render-activity-message-BqgmNPCz.cjs +0 -85
- 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
|
-
|
|
49
|
-
|
|
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
|
|
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:
|
|
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
|
}
|