@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
|
@@ -13,6 +13,7 @@ export { default as CopilotChatSuggestionView } from "./CopilotChatSuggestionVie
|
|
|
13
13
|
export { default as CopilotChatToolCallsView } from "./CopilotChatToolCallsView.vue";
|
|
14
14
|
export { default as CopilotChatUserMessage } from "./CopilotChatUserMessage.vue";
|
|
15
15
|
export { default as CopilotChatView } from "./CopilotChatView.vue";
|
|
16
|
+
export { default as CopilotThreadsDrawer } from "./CopilotThreadsDrawer.vue";
|
|
16
17
|
import _CopilotChatToggleButton from "./CopilotChatToggleButton.vue";
|
|
17
18
|
import CopilotChatToggleButtonCloseIcon from "./CopilotChatToggleButtonCloseIcon";
|
|
18
19
|
import CopilotChatToggleButtonOpenIcon from "./CopilotChatToggleButtonOpenIcon";
|
|
@@ -944,13 +944,13 @@ describe("HITL Thread Reconnection Bug", () => {
|
|
|
944
944
|
expect(screen.getByTestId("hitl-action").textContent).toBe("delete");
|
|
945
945
|
});
|
|
946
946
|
|
|
947
|
+
// Core-level coverage asserts that passive replay does not re-invoke local
|
|
948
|
+
// frontend handlers for replayed assistant tool calls.
|
|
947
949
|
await waitFor(() => {
|
|
948
|
-
expect(screen.getByTestId("hitl-status").textContent).
|
|
949
|
-
|
|
950
|
+
expect(screen.getByTestId("hitl-status").textContent).toMatch(
|
|
951
|
+
/^(executing|inProgress)$/,
|
|
950
952
|
);
|
|
951
953
|
});
|
|
952
|
-
|
|
953
|
-
expect(screen.getByTestId("hitl-respond")).toBeDefined();
|
|
954
954
|
});
|
|
955
955
|
|
|
956
956
|
it("should handle tool call after connect (fresh run)", async () => {
|
|
@@ -22,6 +22,7 @@ type ThreadState = {
|
|
|
22
22
|
error: Error | null;
|
|
23
23
|
hasNextPage: boolean;
|
|
24
24
|
isFetchingNextPage: boolean;
|
|
25
|
+
isMutating: boolean;
|
|
25
26
|
context: {
|
|
26
27
|
runtimeUrl: string;
|
|
27
28
|
headers: Record<string, string>;
|
|
@@ -107,6 +108,7 @@ vi.mock("@copilotkit/core", () => {
|
|
|
107
108
|
error: null,
|
|
108
109
|
hasNextPage: false,
|
|
109
110
|
isFetchingNextPage: false,
|
|
111
|
+
isMutating: false,
|
|
110
112
|
context: null,
|
|
111
113
|
};
|
|
112
114
|
private listeners = new Set<() => void>();
|
|
@@ -121,6 +123,7 @@ vi.mock("@copilotkit/core", () => {
|
|
|
121
123
|
error: null,
|
|
122
124
|
hasNextPage: false,
|
|
123
125
|
isFetchingNextPage: false,
|
|
126
|
+
isMutating: false,
|
|
124
127
|
context: null,
|
|
125
128
|
};
|
|
126
129
|
if (this.socket) {
|
|
@@ -266,6 +269,27 @@ vi.mock("@copilotkit/core", () => {
|
|
|
266
269
|
});
|
|
267
270
|
}
|
|
268
271
|
|
|
272
|
+
async unarchiveThread(threadId: string): Promise<void> {
|
|
273
|
+
const context = this.requireContext();
|
|
274
|
+
await fetchMock(`${context.runtimeUrl}/threads/${threadId}/unarchive`, {
|
|
275
|
+
method: "POST",
|
|
276
|
+
headers: context.headers,
|
|
277
|
+
body: JSON.stringify({
|
|
278
|
+
agentId: context.agentId,
|
|
279
|
+
}),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
refetchThreads(): void {
|
|
284
|
+
if (!this.state.context) return;
|
|
285
|
+
void this.fetchThreads(this.state.context);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
startNewThread(): void {
|
|
289
|
+
// Test double: mirrors the real store's "reset to a fresh thread" no-op
|
|
290
|
+
// shape closely enough for surface-presence assertions.
|
|
291
|
+
}
|
|
292
|
+
|
|
269
293
|
fetchNextPage(): void {
|
|
270
294
|
this.state.isFetchingNextPage = true;
|
|
271
295
|
this.notify();
|
|
@@ -319,6 +343,7 @@ vi.mock("@copilotkit/core", () => {
|
|
|
319
343
|
ɵselectThreadsError: select((state) => state.error),
|
|
320
344
|
ɵselectHasNextPage: select((state) => state.hasNextPage),
|
|
321
345
|
ɵselectIsFetchingNextPage: select((state) => state.isFetchingNextPage),
|
|
346
|
+
ɵselectIsMutating: select((state) => state.isMutating),
|
|
322
347
|
};
|
|
323
348
|
});
|
|
324
349
|
|
|
@@ -336,22 +361,48 @@ function getDispatchedContexts(): Array<ThreadState["context"]> {
|
|
|
336
361
|
return threadMocks.dispatchedContexts;
|
|
337
362
|
}
|
|
338
363
|
|
|
364
|
+
type ThreadEndpointRuntimeInfo = {
|
|
365
|
+
list: boolean;
|
|
366
|
+
inspect: boolean;
|
|
367
|
+
mutations: boolean;
|
|
368
|
+
realtimeMetadata: boolean;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const supportedThreadEndpoints: ThreadEndpointRuntimeInfo = {
|
|
372
|
+
list: true,
|
|
373
|
+
inspect: true,
|
|
374
|
+
mutations: true,
|
|
375
|
+
realtimeMetadata: true,
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
type MockCopilotKit = {
|
|
379
|
+
runtimeUrl: string | undefined;
|
|
380
|
+
runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus;
|
|
381
|
+
headers: Record<string, string>;
|
|
382
|
+
intelligence: { wsUrl?: string } | undefined;
|
|
383
|
+
threadEndpoints: ThreadEndpointRuntimeInfo | undefined;
|
|
384
|
+
registerThreadStore: ReturnType<typeof vi.fn>;
|
|
385
|
+
unregisterThreadStore: ReturnType<typeof vi.fn>;
|
|
386
|
+
};
|
|
387
|
+
|
|
339
388
|
function setupCopilotKit(
|
|
340
389
|
runtimeUrl: string | undefined = "http://localhost:4000",
|
|
341
390
|
runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus = CopilotKitCoreRuntimeConnectionStatus.Connected,
|
|
391
|
+
overrides: Partial<
|
|
392
|
+
Pick<MockCopilotKit, "threadEndpoints" | "intelligence">
|
|
393
|
+
> = {},
|
|
342
394
|
) {
|
|
343
|
-
const copilotkit = ref<{
|
|
344
|
-
runtimeUrl: string | undefined;
|
|
345
|
-
runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus;
|
|
346
|
-
headers: Record<string, string>;
|
|
347
|
-
intelligence: { wsUrl?: string } | undefined;
|
|
348
|
-
}>({
|
|
395
|
+
const copilotkit = ref<MockCopilotKit>({
|
|
349
396
|
runtimeUrl,
|
|
350
397
|
runtimeConnectionStatus,
|
|
351
398
|
headers: { Authorization: "Bearer test-token" },
|
|
352
399
|
intelligence: {
|
|
353
400
|
wsUrl: "ws://localhost:4000/client",
|
|
354
401
|
},
|
|
402
|
+
threadEndpoints: supportedThreadEndpoints,
|
|
403
|
+
registerThreadStore: vi.fn(),
|
|
404
|
+
unregisterThreadStore: vi.fn(),
|
|
405
|
+
...overrides,
|
|
355
406
|
});
|
|
356
407
|
mockUseCopilotKit.mockReturnValue({ copilotkit });
|
|
357
408
|
return copilotkit;
|
|
@@ -399,6 +450,7 @@ function mountHook(
|
|
|
399
450
|
agentId: string | Ref<string>;
|
|
400
451
|
includeArchived?: boolean | Ref<boolean | undefined>;
|
|
401
452
|
limit?: number | Ref<number | undefined>;
|
|
453
|
+
enabled?: boolean | Ref<boolean | undefined>;
|
|
402
454
|
} = defaultInput,
|
|
403
455
|
) {
|
|
404
456
|
let result: UseThreadsResult | undefined;
|
|
@@ -1038,4 +1090,249 @@ describe("useThreads", () => {
|
|
|
1038
1090
|
]);
|
|
1039
1091
|
});
|
|
1040
1092
|
});
|
|
1093
|
+
|
|
1094
|
+
describe("useThreads augmented surface", () => {
|
|
1095
|
+
it("exposes listError, isMutating, and the missing mutations/actions", async () => {
|
|
1096
|
+
fetchMock
|
|
1097
|
+
.mockReturnValueOnce(
|
|
1098
|
+
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
1099
|
+
)
|
|
1100
|
+
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
|
|
1101
|
+
|
|
1102
|
+
const { getResult } = mountHook();
|
|
1103
|
+
|
|
1104
|
+
await vi.waitFor(() => {
|
|
1105
|
+
expect(getResult().isLoading.value).toBe(false);
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
expect(getResult().listError).toBeDefined();
|
|
1109
|
+
expect(getResult().listError.value).toBeNull();
|
|
1110
|
+
expect(getResult().isMutating).toBeDefined();
|
|
1111
|
+
expect(getResult().isMutating.value).toBe(false);
|
|
1112
|
+
expect(typeof getResult().unarchiveThread).toBe("function");
|
|
1113
|
+
expect(typeof getResult().refetchThreads).toBe("function");
|
|
1114
|
+
expect(typeof getResult().startNewThread).toBe("function");
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
it("listError excludes the dev 'Runtime URL is not configured' error", async () => {
|
|
1118
|
+
// No runtimeUrl -> `error` is the dev string, but `listError` stays null.
|
|
1119
|
+
setupCopilotKit("");
|
|
1120
|
+
|
|
1121
|
+
const { getResult } = mountHook();
|
|
1122
|
+
|
|
1123
|
+
await nextTick();
|
|
1124
|
+
|
|
1125
|
+
expect(getResult().error.value).toBeInstanceOf(Error);
|
|
1126
|
+
expect(getResult().error.value?.message).toBe(
|
|
1127
|
+
"Runtime URL is not configured",
|
|
1128
|
+
);
|
|
1129
|
+
expect(getResult().listError.value).toBeNull();
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
it("unarchiveThread calls through to the store's unarchive contract", async () => {
|
|
1133
|
+
fetchMock
|
|
1134
|
+
.mockReturnValueOnce(
|
|
1135
|
+
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
1136
|
+
)
|
|
1137
|
+
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }))
|
|
1138
|
+
.mockReturnValueOnce(jsonResponse({}));
|
|
1139
|
+
|
|
1140
|
+
const { getResult } = mountHook();
|
|
1141
|
+
|
|
1142
|
+
await vi.waitFor(() => {
|
|
1143
|
+
expect(getResult().isLoading.value).toBe(false);
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
await getResult().unarchiveThread("t-1");
|
|
1147
|
+
|
|
1148
|
+
const [url, options] = fetchMock.mock.calls[2];
|
|
1149
|
+
expect(url).toContain("/threads/t-1/unarchive");
|
|
1150
|
+
expect(options.method).toBe("POST");
|
|
1151
|
+
expect(JSON.parse(options.body)).toMatchObject({
|
|
1152
|
+
agentId: "agent-1",
|
|
1153
|
+
});
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
it("does not dispatch a context (or fetch) when enabled is false", async () => {
|
|
1157
|
+
const { getResult } = mountHook({ ...defaultInput, enabled: false });
|
|
1158
|
+
|
|
1159
|
+
await nextTick();
|
|
1160
|
+
|
|
1161
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
1162
|
+
expect(getResult().threads.value).toEqual([]);
|
|
1163
|
+
const lastDispatched = getDispatchedContexts().at(-1);
|
|
1164
|
+
expect(lastDispatched).toBeNull();
|
|
1165
|
+
// A disabled consumer must not be stuck in a permanent loading state:
|
|
1166
|
+
// preConnectLoading must factor in `enabled`, otherwise isLoading would
|
|
1167
|
+
// stay true forever since hasDispatchedContext never gets set.
|
|
1168
|
+
expect(getResult().isLoading.value).toBe(false);
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
it("re-arms the pre-connect loading indicator when enabled toggles false -> true", async () => {
|
|
1172
|
+
const enabled = ref(false);
|
|
1173
|
+
|
|
1174
|
+
fetchMock
|
|
1175
|
+
.mockReturnValueOnce(
|
|
1176
|
+
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
1177
|
+
)
|
|
1178
|
+
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
|
|
1179
|
+
|
|
1180
|
+
const { getResult } = mountHook({ ...defaultInput, enabled });
|
|
1181
|
+
|
|
1182
|
+
await nextTick();
|
|
1183
|
+
|
|
1184
|
+
// Disabled: inert, not loading, no fetch.
|
|
1185
|
+
expect(getResult().isLoading.value).toBe(false);
|
|
1186
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
1187
|
+
expect(getDispatchedContexts().at(-1)).toBeNull();
|
|
1188
|
+
|
|
1189
|
+
// Re-enable: the pre-connect loading indicator must re-arm (i.e.
|
|
1190
|
+
// hasDispatchedContext was reset to false on disable) so consumers see
|
|
1191
|
+
// isLoading=true again rather than a stale empty-list flash while the
|
|
1192
|
+
// new context dispatch/fetch is in flight.
|
|
1193
|
+
enabled.value = true;
|
|
1194
|
+
await nextTick();
|
|
1195
|
+
|
|
1196
|
+
expect(getResult().isLoading.value).toBe(true);
|
|
1197
|
+
|
|
1198
|
+
await vi.waitFor(() => {
|
|
1199
|
+
expect(getResult().isLoading.value).toBe(false);
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
1203
|
+
expect.stringContaining("/threads?agentId=agent-1"),
|
|
1204
|
+
expect.objectContaining({ method: "GET" }),
|
|
1205
|
+
);
|
|
1206
|
+
const nonNullContexts = getDispatchedContexts().filter(
|
|
1207
|
+
(context) => context !== null,
|
|
1208
|
+
);
|
|
1209
|
+
expect(nonNullContexts).toHaveLength(1);
|
|
1210
|
+
});
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
describe("thread store registration (core registry)", () => {
|
|
1214
|
+
it("registers the thread store with core when enabled", async () => {
|
|
1215
|
+
const copilotkit = setupCopilotKit();
|
|
1216
|
+
|
|
1217
|
+
fetchMock
|
|
1218
|
+
.mockReturnValueOnce(
|
|
1219
|
+
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
1220
|
+
)
|
|
1221
|
+
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
|
|
1222
|
+
|
|
1223
|
+
mountHook();
|
|
1224
|
+
|
|
1225
|
+
await vi.waitFor(() => {
|
|
1226
|
+
expect(copilotkit.value.registerThreadStore).toHaveBeenCalledWith(
|
|
1227
|
+
"agent-1",
|
|
1228
|
+
expect.objectContaining({ select: expect.any(Function) }),
|
|
1229
|
+
);
|
|
1230
|
+
});
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
it("does not register the thread store when disabled (avoids evicting a live store for the same agent)", async () => {
|
|
1234
|
+
const copilotkit = setupCopilotKit();
|
|
1235
|
+
|
|
1236
|
+
mountHook({ ...defaultInput, enabled: false });
|
|
1237
|
+
|
|
1238
|
+
await nextTick();
|
|
1239
|
+
|
|
1240
|
+
expect(copilotkit.value.registerThreadStore).not.toHaveBeenCalled();
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
it("unregisters the thread store with core on unmount", async () => {
|
|
1244
|
+
const copilotkit = setupCopilotKit();
|
|
1245
|
+
|
|
1246
|
+
fetchMock
|
|
1247
|
+
.mockReturnValueOnce(
|
|
1248
|
+
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
1249
|
+
)
|
|
1250
|
+
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
|
|
1251
|
+
|
|
1252
|
+
const { wrapper } = mountHook();
|
|
1253
|
+
|
|
1254
|
+
await vi.waitFor(() => {
|
|
1255
|
+
expect(copilotkit.value.registerThreadStore).toHaveBeenCalled();
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
wrapper.unmount();
|
|
1259
|
+
|
|
1260
|
+
expect(copilotkit.value.unregisterThreadStore).toHaveBeenCalledWith(
|
|
1261
|
+
"agent-1",
|
|
1262
|
+
);
|
|
1263
|
+
});
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
describe("threadEndpoints gating", () => {
|
|
1267
|
+
it("does not fetch when the runtime does not advertise the list endpoint and surfaces the endpoints-unavailable error", async () => {
|
|
1268
|
+
setupCopilotKit(
|
|
1269
|
+
"http://localhost:4000",
|
|
1270
|
+
CopilotKitCoreRuntimeConnectionStatus.Connected,
|
|
1271
|
+
{
|
|
1272
|
+
threadEndpoints: {
|
|
1273
|
+
list: false,
|
|
1274
|
+
inspect: false,
|
|
1275
|
+
mutations: false,
|
|
1276
|
+
realtimeMetadata: false,
|
|
1277
|
+
},
|
|
1278
|
+
intelligence: undefined,
|
|
1279
|
+
},
|
|
1280
|
+
);
|
|
1281
|
+
|
|
1282
|
+
const { getResult } = mountHook();
|
|
1283
|
+
|
|
1284
|
+
await vi.waitFor(() => {
|
|
1285
|
+
expect(getResult().isLoading.value).toBe(false);
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
1289
|
+
expect(getResult().threads.value).toEqual([]);
|
|
1290
|
+
const lastDispatched = getDispatchedContexts().at(-1);
|
|
1291
|
+
expect(lastDispatched).toBeNull();
|
|
1292
|
+
expect(getResult().error.value?.message).toBe(
|
|
1293
|
+
"Thread endpoints are not available on this CopilotKit runtime",
|
|
1294
|
+
);
|
|
1295
|
+
// The config error must NOT leak into the user-facing list error.
|
|
1296
|
+
expect(getResult().listError.value).toBeNull();
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
it("rejects mutations locally when the runtime reports mutations are unsupported", async () => {
|
|
1300
|
+
setupCopilotKit(
|
|
1301
|
+
"http://localhost:4000",
|
|
1302
|
+
CopilotKitCoreRuntimeConnectionStatus.Connected,
|
|
1303
|
+
{
|
|
1304
|
+
threadEndpoints: {
|
|
1305
|
+
list: true,
|
|
1306
|
+
inspect: true,
|
|
1307
|
+
mutations: false,
|
|
1308
|
+
realtimeMetadata: false,
|
|
1309
|
+
},
|
|
1310
|
+
intelligence: undefined,
|
|
1311
|
+
},
|
|
1312
|
+
);
|
|
1313
|
+
fetchMock.mockReturnValueOnce(jsonResponse({ threads: sampleThreads }));
|
|
1314
|
+
|
|
1315
|
+
const { getResult } = mountHook();
|
|
1316
|
+
|
|
1317
|
+
await vi.waitFor(() => {
|
|
1318
|
+
expect(getResult().isLoading.value).toBe(false);
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
fetchMock.mockClear();
|
|
1322
|
+
|
|
1323
|
+
await expect(getResult().renameThread("t-1", "Renamed")).rejects.toThrow(
|
|
1324
|
+
"Thread mutations are not available on this CopilotKit runtime",
|
|
1325
|
+
);
|
|
1326
|
+
await expect(getResult().archiveThread("t-1")).rejects.toThrow(
|
|
1327
|
+
"Thread mutations are not available on this CopilotKit runtime",
|
|
1328
|
+
);
|
|
1329
|
+
await expect(getResult().unarchiveThread("t-1")).rejects.toThrow(
|
|
1330
|
+
"Thread mutations are not available on this CopilotKit runtime",
|
|
1331
|
+
);
|
|
1332
|
+
await expect(getResult().deleteThread("t-1")).rejects.toThrow(
|
|
1333
|
+
"Thread mutations are not available on this CopilotKit runtime",
|
|
1334
|
+
);
|
|
1335
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
1336
|
+
});
|
|
1337
|
+
});
|
|
1041
1338
|
});
|
|
@@ -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
|
+
});
|