@copilotkit/vue 1.62.0 → 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/PARITY.md +7 -7
- 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-Bzh3Bckz.js → use-render-activity-message-DbGuwvBd.js} +3363 -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-agent.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 +7 -6
- 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-agent.test.ts +60 -1
- 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-agent.ts +4 -1
- 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-Bzh3Bckz.js.map +0 -1
- package/dist/use-render-activity-message-CW3vipBB.cjs +0 -85
- package/dist/use-render-activity-message-CW3vipBB.cjs.map +0 -1
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ɵcreateThreadStore,
|
|
6
6
|
ɵselectHasNextPage,
|
|
7
7
|
ɵselectIsFetchingNextPage,
|
|
8
|
+
ɵselectIsMutating,
|
|
8
9
|
ɵselectThreads,
|
|
9
10
|
ɵselectThreadsError,
|
|
10
11
|
ɵselectThreadsIsLoading,
|
|
@@ -32,17 +33,29 @@ export interface UseThreadsInput {
|
|
|
32
33
|
agentId: MaybeRefOrGetter<string>;
|
|
33
34
|
includeArchived?: MaybeRefOrGetter<boolean | undefined>;
|
|
34
35
|
limit?: MaybeRefOrGetter<number | undefined>;
|
|
36
|
+
/**
|
|
37
|
+
* When `false`, the hook clears the store context and issues no `/threads`
|
|
38
|
+
* request — used by the drawer to skip fetching while unlicensed. Defaults
|
|
39
|
+
* to `true`.
|
|
40
|
+
*/
|
|
41
|
+
enabled?: MaybeRefOrGetter<boolean | undefined>;
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
export interface UseThreadsResult {
|
|
38
45
|
threads: Ref<Thread[]>;
|
|
39
46
|
isLoading: Ref<boolean>;
|
|
40
47
|
error: Ref<Error | null>;
|
|
48
|
+
/** Genuine list-load/mutation errors only — excludes dev/config errors. */
|
|
49
|
+
listError: Ref<Error | null>;
|
|
41
50
|
hasMoreThreads: Ref<boolean>;
|
|
42
51
|
isFetchingMoreThreads: Ref<boolean>;
|
|
52
|
+
isMutating: Ref<boolean>;
|
|
43
53
|
fetchMoreThreads: () => void;
|
|
54
|
+
refetchThreads: () => void;
|
|
55
|
+
startNewThread: () => void;
|
|
44
56
|
renameThread: (threadId: string, name: string) => Promise<void>;
|
|
45
57
|
archiveThread: (threadId: string) => Promise<void>;
|
|
58
|
+
unarchiveThread: (threadId: string) => Promise<void>;
|
|
46
59
|
deleteThread: (threadId: string) => Promise<void>;
|
|
47
60
|
}
|
|
48
61
|
|
|
@@ -77,6 +90,41 @@ export function useThreads(input: UseThreadsInput): UseThreadsResult {
|
|
|
77
90
|
toValue(input.includeArchived),
|
|
78
91
|
);
|
|
79
92
|
const resolvedLimit = computed(() => toValue(input.limit));
|
|
93
|
+
const resolvedEnabled = computed(() => toValue(input.enabled) ?? true);
|
|
94
|
+
|
|
95
|
+
// Thread-endpoint capability, derived from the runtime's `/info` payload.
|
|
96
|
+
// `threadEndpoints` is `undefined` on legacy runtimes that don't advertise
|
|
97
|
+
// the capability, so we treat "not explicitly false" as supported (matching
|
|
98
|
+
// React's `!== false` semantics) to preserve legacy thread behavior.
|
|
99
|
+
const threadListEndpointSupported = computed(
|
|
100
|
+
() => copilotkit.value.threadEndpoints?.list !== false,
|
|
101
|
+
);
|
|
102
|
+
const threadMutationsSupported = computed(
|
|
103
|
+
() => copilotkit.value.threadEndpoints?.mutations !== false,
|
|
104
|
+
);
|
|
105
|
+
// Connected, but the runtime explicitly does not serve the list endpoint.
|
|
106
|
+
const threadEndpointsUnavailable = computed(
|
|
107
|
+
() =>
|
|
108
|
+
!!copilotkit.value.runtimeUrl &&
|
|
109
|
+
copilotkit.value.runtimeConnectionStatus ===
|
|
110
|
+
CopilotKitCoreRuntimeConnectionStatus.Connected &&
|
|
111
|
+
!threadListEndpointSupported.value,
|
|
112
|
+
);
|
|
113
|
+
const threadEndpointsError = computed<Error | null>(() =>
|
|
114
|
+
threadEndpointsUnavailable.value
|
|
115
|
+
? new Error(
|
|
116
|
+
"Thread endpoints are not available on this CopilotKit runtime",
|
|
117
|
+
)
|
|
118
|
+
: null,
|
|
119
|
+
);
|
|
120
|
+
const threadMutationsError = computed<Error | null>(() =>
|
|
121
|
+
threadMutationsSupported.value
|
|
122
|
+
? null
|
|
123
|
+
: new Error(
|
|
124
|
+
"Thread mutations are not available on this CopilotKit runtime",
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
|
|
80
128
|
const headersKey = computed(() =>
|
|
81
129
|
JSON.stringify(
|
|
82
130
|
Object.entries(copilotkit.value.headers ?? {}).sort(([left], [right]) =>
|
|
@@ -90,6 +138,7 @@ export function useThreads(input: UseThreadsInput): UseThreadsResult {
|
|
|
90
138
|
const storeError = ref<Error | null>(null);
|
|
91
139
|
const hasMoreThreads = ref(false);
|
|
92
140
|
const isFetchingMoreThreads = ref(false);
|
|
141
|
+
const isMutating = ref(false);
|
|
93
142
|
|
|
94
143
|
bindThreadStoreSelector(store, ɵselectThreads, threads as Ref<Thread[]>);
|
|
95
144
|
bindThreadStoreSelector(store, ɵselectThreadsIsLoading, storeIsLoading);
|
|
@@ -100,12 +149,33 @@ export function useThreads(input: UseThreadsInput): UseThreadsResult {
|
|
|
100
149
|
ɵselectIsFetchingNextPage,
|
|
101
150
|
isFetchingMoreThreads,
|
|
102
151
|
);
|
|
152
|
+
bindThreadStoreSelector(store, ɵselectIsMutating, isMutating);
|
|
103
153
|
|
|
104
154
|
store.start();
|
|
105
155
|
onScopeDispose(() => {
|
|
106
156
|
store.stop();
|
|
107
157
|
});
|
|
108
158
|
|
|
159
|
+
// Register this store in core's single-slot thread-store registry so other
|
|
160
|
+
// surfaces (e.g. the chat view) can resolve the same store for the agent.
|
|
161
|
+
//
|
|
162
|
+
// Gated on `resolvedEnabled`: a disabled (e.g. unlicensed) drawer must not
|
|
163
|
+
// claim the agentId slot. The registry is single-slot/last-writer-wins, so
|
|
164
|
+
// registering an inert store would evict — and on unmount tear down — a
|
|
165
|
+
// co-mounted live store for the same agent. Staying unregistered while
|
|
166
|
+
// disabled leaves the live store's registration intact.
|
|
167
|
+
watch(
|
|
168
|
+
[resolvedEnabled, resolvedAgentId],
|
|
169
|
+
([enabled, agentId], _prev, onCleanup) => {
|
|
170
|
+
if (!enabled) return;
|
|
171
|
+
copilotkit.value.registerThreadStore(agentId, store);
|
|
172
|
+
onCleanup(() => {
|
|
173
|
+
copilotkit.value.unregisterThreadStore(agentId);
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
{ immediate: true },
|
|
177
|
+
);
|
|
178
|
+
|
|
109
179
|
// Tracks whether we've dispatched the first real context to the store.
|
|
110
180
|
// The store itself starts with `isLoading: false`, so before we dispatch
|
|
111
181
|
// consumers would otherwise see an empty, non-loading state (empty-list
|
|
@@ -135,10 +205,23 @@ export function useThreads(input: UseThreadsInput): UseThreadsResult {
|
|
|
135
205
|
resolvedAgentId,
|
|
136
206
|
resolvedIncludeArchived,
|
|
137
207
|
resolvedLimit,
|
|
208
|
+
resolvedEnabled,
|
|
209
|
+
threadListEndpointSupported,
|
|
138
210
|
],
|
|
139
|
-
([
|
|
140
|
-
|
|
211
|
+
([
|
|
212
|
+
runtimeUrl,
|
|
213
|
+
runtimeStatus,
|
|
214
|
+
,
|
|
215
|
+
wsUrl,
|
|
216
|
+
agentId,
|
|
217
|
+
includeArchived,
|
|
218
|
+
limit,
|
|
219
|
+
enabled,
|
|
220
|
+
listEndpointSupported,
|
|
221
|
+
]) => {
|
|
222
|
+
if (!runtimeUrl || !enabled) {
|
|
141
223
|
store.setContext(null);
|
|
224
|
+
hasDispatchedContext.value = false;
|
|
142
225
|
return;
|
|
143
226
|
}
|
|
144
227
|
|
|
@@ -146,6 +229,15 @@ export function useThreads(input: UseThreadsInput): UseThreadsResult {
|
|
|
146
229
|
return;
|
|
147
230
|
}
|
|
148
231
|
|
|
232
|
+
// Connected, but the runtime explicitly does not serve the list
|
|
233
|
+
// endpoint: stay inert so no `/threads` fetch fires. The
|
|
234
|
+
// `threadEndpointsError` surfaces via `error` instead.
|
|
235
|
+
if (!listEndpointSupported) {
|
|
236
|
+
store.setContext(null);
|
|
237
|
+
hasDispatchedContext.value = false;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
149
241
|
const context: ɵThreadRuntimeContext = {
|
|
150
242
|
runtimeUrl,
|
|
151
243
|
headers: { ...copilotkit.value.headers },
|
|
@@ -168,19 +260,44 @@ export function useThreads(input: UseThreadsInput): UseThreadsResult {
|
|
|
168
260
|
);
|
|
169
261
|
|
|
170
262
|
const preConnectLoading = computed(
|
|
171
|
-
() =>
|
|
263
|
+
() =>
|
|
264
|
+
!!copilotkit.value.runtimeUrl &&
|
|
265
|
+
resolvedEnabled.value &&
|
|
266
|
+
!threadEndpointsUnavailable.value &&
|
|
267
|
+
!hasDispatchedContext.value,
|
|
172
268
|
);
|
|
173
269
|
|
|
174
270
|
const isLoading = computed(() =>
|
|
175
|
-
runtimeError.value
|
|
271
|
+
runtimeError.value || threadEndpointsError.value
|
|
176
272
|
? false
|
|
177
273
|
: preConnectLoading.value || storeIsLoading.value,
|
|
178
274
|
);
|
|
179
275
|
|
|
276
|
+
// Folds developer/config errors (missing runtime URL, runtime without
|
|
277
|
+
// thread endpoints) together with genuine store errors. `listError` below
|
|
278
|
+
// deliberately excludes the config errors.
|
|
180
279
|
const error = computed<Error | null>(
|
|
181
|
-
() => runtimeError.value ?? storeError.value,
|
|
280
|
+
() => runtimeError.value ?? threadEndpointsError.value ?? storeError.value,
|
|
182
281
|
);
|
|
183
282
|
|
|
283
|
+
// Genuine store errors only. Unlike `error`, this omits the dev/config
|
|
284
|
+
// `runtimeError`/`threadEndpointsError` strings so they are never shown in
|
|
285
|
+
// user-facing error UI.
|
|
286
|
+
const listError = computed<Error | null>(() => storeError.value);
|
|
287
|
+
|
|
288
|
+
// Reject mutations locally (before touching the network) when the runtime
|
|
289
|
+
// reports mutations are unsupported, matching React's `guardMutation`.
|
|
290
|
+
function guardMutation<TArgs extends unknown[]>(
|
|
291
|
+
mutation: (...args: TArgs) => Promise<void>,
|
|
292
|
+
): (...args: TArgs) => Promise<void> {
|
|
293
|
+
return (...args: TArgs) => {
|
|
294
|
+
if (threadMutationsError.value) {
|
|
295
|
+
return Promise.reject(threadMutationsError.value);
|
|
296
|
+
}
|
|
297
|
+
return mutation(...args);
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
184
301
|
return {
|
|
185
302
|
threads: computed(() =>
|
|
186
303
|
threads.value.map(
|
|
@@ -197,12 +314,24 @@ export function useThreads(input: UseThreadsInput): UseThreadsResult {
|
|
|
197
314
|
),
|
|
198
315
|
isLoading,
|
|
199
316
|
error,
|
|
317
|
+
listError,
|
|
200
318
|
hasMoreThreads,
|
|
201
319
|
isFetchingMoreThreads,
|
|
320
|
+
isMutating,
|
|
202
321
|
fetchMoreThreads: () => store.fetchNextPage(),
|
|
203
|
-
|
|
322
|
+
refetchThreads: () => store.refetchThreads(),
|
|
323
|
+
startNewThread: () => store.startNewThread(),
|
|
324
|
+
renameThread: guardMutation((threadId: string, name: string) =>
|
|
204
325
|
store.renameThread(threadId, name),
|
|
205
|
-
|
|
206
|
-
|
|
326
|
+
),
|
|
327
|
+
archiveThread: guardMutation((threadId: string) =>
|
|
328
|
+
store.archiveThread(threadId),
|
|
329
|
+
),
|
|
330
|
+
unarchiveThread: guardMutation((threadId: string) =>
|
|
331
|
+
store.unarchiveThread(threadId),
|
|
332
|
+
),
|
|
333
|
+
deleteThread: guardMutation((threadId: string) =>
|
|
334
|
+
store.deleteThread(threadId),
|
|
335
|
+
),
|
|
207
336
|
};
|
|
208
337
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { isMobileViewport } from "../is-mobile-viewport";
|
|
3
|
+
|
|
4
|
+
describe("isMobileViewport", () => {
|
|
5
|
+
const originalMatchMedia = window.matchMedia;
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
window.matchMedia = originalMatchMedia;
|
|
9
|
+
vi.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("returns false when window.matchMedia is undefined", () => {
|
|
13
|
+
// @ts-expect-error - simulating an environment without matchMedia support.
|
|
14
|
+
delete window.matchMedia;
|
|
15
|
+
|
|
16
|
+
expect(isMobileViewport()).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("returns true when the mobile media query matches", () => {
|
|
20
|
+
window.matchMedia = vi.fn().mockReturnValue({
|
|
21
|
+
matches: true,
|
|
22
|
+
media: "(max-width: 767px)",
|
|
23
|
+
onchange: null,
|
|
24
|
+
addEventListener: vi.fn(),
|
|
25
|
+
removeEventListener: vi.fn(),
|
|
26
|
+
addListener: vi.fn(),
|
|
27
|
+
removeListener: vi.fn(),
|
|
28
|
+
dispatchEvent: vi.fn(),
|
|
29
|
+
} as unknown as MediaQueryList);
|
|
30
|
+
|
|
31
|
+
expect(isMobileViewport()).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("returns false when the mobile media query does not match", () => {
|
|
35
|
+
window.matchMedia = vi.fn().mockReturnValue({
|
|
36
|
+
matches: false,
|
|
37
|
+
media: "(max-width: 767px)",
|
|
38
|
+
onchange: null,
|
|
39
|
+
addEventListener: vi.fn(),
|
|
40
|
+
removeEventListener: vi.fn(),
|
|
41
|
+
addListener: vi.fn(),
|
|
42
|
+
removeListener: vi.fn(),
|
|
43
|
+
dispatchEvent: vi.fn(),
|
|
44
|
+
} as unknown as MediaQueryList);
|
|
45
|
+
|
|
46
|
+
expect(isMobileViewport()).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -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
|
}
|