@copilotkit/vue 1.62.1 → 1.62.2-canary.1784333495
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.cjs +1 -1
- package/dist/index.mjs +70 -69
- package/dist/styles.css +1 -1
- package/dist/use-render-activity-message-CIvEoQzA.cjs +85 -0
- package/dist/use-render-activity-message-CIvEoQzA.cjs.map +1 -0
- package/dist/{use-render-activity-message-8_HyVZWc.js → use-render-activity-message-CbKeqosY.js} +3514 -3207
- package/dist/use-render-activity-message-CbKeqosY.js.map +1 -0
- package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts +8 -0
- package/dist/v2/components/chat/CopilotChatAssistantMessage.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 +65 -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 +19 -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 +335 -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 +910 -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 +315 -6
- package/src/v2/hooks/use-threads.ts +148 -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/vite.config.ts +7 -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 () => {
|
|
@@ -20,8 +20,10 @@ type ThreadState = {
|
|
|
20
20
|
threads: ThreadRecord[];
|
|
21
21
|
isLoading: boolean;
|
|
22
22
|
error: Error | null;
|
|
23
|
+
fetchMoreError: Error | null;
|
|
23
24
|
hasNextPage: boolean;
|
|
24
25
|
isFetchingNextPage: boolean;
|
|
26
|
+
isMutating: boolean;
|
|
25
27
|
context: {
|
|
26
28
|
runtimeUrl: string;
|
|
27
29
|
headers: Record<string, string>;
|
|
@@ -105,8 +107,10 @@ vi.mock("@copilotkit/core", () => {
|
|
|
105
107
|
threads: [],
|
|
106
108
|
isLoading: false,
|
|
107
109
|
error: null,
|
|
110
|
+
fetchMoreError: null,
|
|
108
111
|
hasNextPage: false,
|
|
109
112
|
isFetchingNextPage: false,
|
|
113
|
+
isMutating: false,
|
|
110
114
|
context: null,
|
|
111
115
|
};
|
|
112
116
|
private listeners = new Set<() => void>();
|
|
@@ -119,8 +123,10 @@ vi.mock("@copilotkit/core", () => {
|
|
|
119
123
|
threads: [],
|
|
120
124
|
isLoading: false,
|
|
121
125
|
error: null,
|
|
126
|
+
fetchMoreError: null,
|
|
122
127
|
hasNextPage: false,
|
|
123
128
|
isFetchingNextPage: false,
|
|
129
|
+
isMutating: false,
|
|
124
130
|
context: null,
|
|
125
131
|
};
|
|
126
132
|
if (this.socket) {
|
|
@@ -266,6 +272,31 @@ vi.mock("@copilotkit/core", () => {
|
|
|
266
272
|
});
|
|
267
273
|
}
|
|
268
274
|
|
|
275
|
+
async unarchiveThread(threadId: string): Promise<void> {
|
|
276
|
+
const context = this.requireContext();
|
|
277
|
+
// Mirrors the real core store: unarchive is a PATCH to the thread with
|
|
278
|
+
// `{ archived: false }`, NOT a dedicated /unarchive sub-route (see
|
|
279
|
+
// core/src/__tests__/threads.test.ts).
|
|
280
|
+
await fetchMock(`${context.runtimeUrl}/threads/${threadId}`, {
|
|
281
|
+
method: "PATCH",
|
|
282
|
+
headers: context.headers,
|
|
283
|
+
body: JSON.stringify({
|
|
284
|
+
agentId: context.agentId,
|
|
285
|
+
archived: false,
|
|
286
|
+
}),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
refetchThreads(): void {
|
|
291
|
+
if (!this.state.context) return;
|
|
292
|
+
void this.fetchThreads(this.state.context);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
startNewThread(): void {
|
|
296
|
+
// Test double: mirrors the real store's "reset to a fresh thread" no-op
|
|
297
|
+
// shape closely enough for surface-presence assertions.
|
|
298
|
+
}
|
|
299
|
+
|
|
269
300
|
fetchNextPage(): void {
|
|
270
301
|
this.state.isFetchingNextPage = true;
|
|
271
302
|
this.notify();
|
|
@@ -317,8 +348,10 @@ vi.mock("@copilotkit/core", () => {
|
|
|
317
348
|
ɵselectThreads: select((state) => state.threads),
|
|
318
349
|
ɵselectThreadsIsLoading: select((state) => state.isLoading),
|
|
319
350
|
ɵselectThreadsError: select((state) => state.error),
|
|
351
|
+
ɵselectFetchMoreError: select((state) => state.fetchMoreError),
|
|
320
352
|
ɵselectHasNextPage: select((state) => state.hasNextPage),
|
|
321
353
|
ɵselectIsFetchingNextPage: select((state) => state.isFetchingNextPage),
|
|
354
|
+
ɵselectIsMutating: select((state) => state.isMutating),
|
|
322
355
|
};
|
|
323
356
|
});
|
|
324
357
|
|
|
@@ -336,22 +369,48 @@ function getDispatchedContexts(): Array<ThreadState["context"]> {
|
|
|
336
369
|
return threadMocks.dispatchedContexts;
|
|
337
370
|
}
|
|
338
371
|
|
|
372
|
+
type ThreadEndpointRuntimeInfo = {
|
|
373
|
+
list: boolean;
|
|
374
|
+
inspect: boolean;
|
|
375
|
+
mutations: boolean;
|
|
376
|
+
realtimeMetadata: boolean;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const supportedThreadEndpoints: ThreadEndpointRuntimeInfo = {
|
|
380
|
+
list: true,
|
|
381
|
+
inspect: true,
|
|
382
|
+
mutations: true,
|
|
383
|
+
realtimeMetadata: true,
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
type MockCopilotKit = {
|
|
387
|
+
runtimeUrl: string | undefined;
|
|
388
|
+
runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus;
|
|
389
|
+
headers: Record<string, string>;
|
|
390
|
+
intelligence: { wsUrl?: string } | undefined;
|
|
391
|
+
threadEndpoints: ThreadEndpointRuntimeInfo | undefined;
|
|
392
|
+
registerThreadStore: ReturnType<typeof vi.fn>;
|
|
393
|
+
unregisterThreadStore: ReturnType<typeof vi.fn>;
|
|
394
|
+
};
|
|
395
|
+
|
|
339
396
|
function setupCopilotKit(
|
|
340
397
|
runtimeUrl: string | undefined = "http://localhost:4000",
|
|
341
398
|
runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus = CopilotKitCoreRuntimeConnectionStatus.Connected,
|
|
399
|
+
overrides: Partial<
|
|
400
|
+
Pick<MockCopilotKit, "threadEndpoints" | "intelligence">
|
|
401
|
+
> = {},
|
|
342
402
|
) {
|
|
343
|
-
const copilotkit = ref<{
|
|
344
|
-
runtimeUrl: string | undefined;
|
|
345
|
-
runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus;
|
|
346
|
-
headers: Record<string, string>;
|
|
347
|
-
intelligence: { wsUrl?: string } | undefined;
|
|
348
|
-
}>({
|
|
403
|
+
const copilotkit = ref<MockCopilotKit>({
|
|
349
404
|
runtimeUrl,
|
|
350
405
|
runtimeConnectionStatus,
|
|
351
406
|
headers: { Authorization: "Bearer test-token" },
|
|
352
407
|
intelligence: {
|
|
353
408
|
wsUrl: "ws://localhost:4000/client",
|
|
354
409
|
},
|
|
410
|
+
threadEndpoints: supportedThreadEndpoints,
|
|
411
|
+
registerThreadStore: vi.fn(),
|
|
412
|
+
unregisterThreadStore: vi.fn(),
|
|
413
|
+
...overrides,
|
|
355
414
|
});
|
|
356
415
|
mockUseCopilotKit.mockReturnValue({ copilotkit });
|
|
357
416
|
return copilotkit;
|
|
@@ -399,6 +458,7 @@ function mountHook(
|
|
|
399
458
|
agentId: string | Ref<string>;
|
|
400
459
|
includeArchived?: boolean | Ref<boolean | undefined>;
|
|
401
460
|
limit?: number | Ref<number | undefined>;
|
|
461
|
+
enabled?: boolean | Ref<boolean | undefined>;
|
|
402
462
|
} = defaultInput,
|
|
403
463
|
) {
|
|
404
464
|
let result: UseThreadsResult | undefined;
|
|
@@ -1038,4 +1098,253 @@ describe("useThreads", () => {
|
|
|
1038
1098
|
]);
|
|
1039
1099
|
});
|
|
1040
1100
|
});
|
|
1101
|
+
|
|
1102
|
+
describe("useThreads augmented surface", () => {
|
|
1103
|
+
it("exposes listError, isMutating, and the missing mutations/actions", async () => {
|
|
1104
|
+
fetchMock
|
|
1105
|
+
.mockReturnValueOnce(
|
|
1106
|
+
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
1107
|
+
)
|
|
1108
|
+
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
|
|
1109
|
+
|
|
1110
|
+
const { getResult } = mountHook();
|
|
1111
|
+
|
|
1112
|
+
await vi.waitFor(() => {
|
|
1113
|
+
expect(getResult().isLoading.value).toBe(false);
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
expect(getResult().listError).toBeDefined();
|
|
1117
|
+
expect(getResult().listError.value).toBeNull();
|
|
1118
|
+
expect(getResult().isMutating).toBeDefined();
|
|
1119
|
+
expect(getResult().isMutating.value).toBe(false);
|
|
1120
|
+
expect(typeof getResult().unarchiveThread).toBe("function");
|
|
1121
|
+
expect(typeof getResult().refetchThreads).toBe("function");
|
|
1122
|
+
expect(typeof getResult().startNewThread).toBe("function");
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
it("listError excludes the dev 'Runtime URL is not configured' error", async () => {
|
|
1126
|
+
// No runtimeUrl -> `error` is the dev string, but `listError` stays null.
|
|
1127
|
+
setupCopilotKit("");
|
|
1128
|
+
|
|
1129
|
+
const { getResult } = mountHook();
|
|
1130
|
+
|
|
1131
|
+
await nextTick();
|
|
1132
|
+
|
|
1133
|
+
expect(getResult().error.value).toBeInstanceOf(Error);
|
|
1134
|
+
expect(getResult().error.value?.message).toBe(
|
|
1135
|
+
"Runtime URL is not configured",
|
|
1136
|
+
);
|
|
1137
|
+
expect(getResult().listError.value).toBeNull();
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
it("unarchiveThread calls through to the store's unarchive contract", async () => {
|
|
1141
|
+
fetchMock
|
|
1142
|
+
.mockReturnValueOnce(
|
|
1143
|
+
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
1144
|
+
)
|
|
1145
|
+
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }))
|
|
1146
|
+
.mockReturnValueOnce(jsonResponse({}));
|
|
1147
|
+
|
|
1148
|
+
const { getResult } = mountHook();
|
|
1149
|
+
|
|
1150
|
+
await vi.waitFor(() => {
|
|
1151
|
+
expect(getResult().isLoading.value).toBe(false);
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
await getResult().unarchiveThread("t-1");
|
|
1155
|
+
|
|
1156
|
+
const [url, options] = fetchMock.mock.calls[2];
|
|
1157
|
+
// Unarchive is a PATCH to the thread with `{ archived: false }` (matching
|
|
1158
|
+
// the real core store), not a dedicated /unarchive sub-route.
|
|
1159
|
+
expect(url).toContain("/threads/t-1");
|
|
1160
|
+
expect(url).not.toContain("/unarchive");
|
|
1161
|
+
expect(options.method).toBe("PATCH");
|
|
1162
|
+
expect(JSON.parse(options.body)).toMatchObject({
|
|
1163
|
+
agentId: "agent-1",
|
|
1164
|
+
archived: false,
|
|
1165
|
+
});
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
it("does not dispatch a context (or fetch) when enabled is false", async () => {
|
|
1169
|
+
const { getResult } = mountHook({ ...defaultInput, enabled: false });
|
|
1170
|
+
|
|
1171
|
+
await nextTick();
|
|
1172
|
+
|
|
1173
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
1174
|
+
expect(getResult().threads.value).toEqual([]);
|
|
1175
|
+
const lastDispatched = getDispatchedContexts().at(-1);
|
|
1176
|
+
expect(lastDispatched).toBeNull();
|
|
1177
|
+
// A disabled consumer must not be stuck in a permanent loading state:
|
|
1178
|
+
// preConnectLoading must factor in `enabled`, otherwise isLoading would
|
|
1179
|
+
// stay true forever since hasDispatchedContext never gets set.
|
|
1180
|
+
expect(getResult().isLoading.value).toBe(false);
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
it("re-arms the pre-connect loading indicator when enabled toggles false -> true", async () => {
|
|
1184
|
+
const enabled = ref(false);
|
|
1185
|
+
|
|
1186
|
+
fetchMock
|
|
1187
|
+
.mockReturnValueOnce(
|
|
1188
|
+
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
1189
|
+
)
|
|
1190
|
+
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
|
|
1191
|
+
|
|
1192
|
+
const { getResult } = mountHook({ ...defaultInput, enabled });
|
|
1193
|
+
|
|
1194
|
+
await nextTick();
|
|
1195
|
+
|
|
1196
|
+
// Disabled: inert, not loading, no fetch.
|
|
1197
|
+
expect(getResult().isLoading.value).toBe(false);
|
|
1198
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
1199
|
+
expect(getDispatchedContexts().at(-1)).toBeNull();
|
|
1200
|
+
|
|
1201
|
+
// Re-enable: the pre-connect loading indicator must re-arm (i.e.
|
|
1202
|
+
// hasDispatchedContext was reset to false on disable) so consumers see
|
|
1203
|
+
// isLoading=true again rather than a stale empty-list flash while the
|
|
1204
|
+
// new context dispatch/fetch is in flight.
|
|
1205
|
+
enabled.value = true;
|
|
1206
|
+
await nextTick();
|
|
1207
|
+
|
|
1208
|
+
expect(getResult().isLoading.value).toBe(true);
|
|
1209
|
+
|
|
1210
|
+
await vi.waitFor(() => {
|
|
1211
|
+
expect(getResult().isLoading.value).toBe(false);
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
1215
|
+
expect.stringContaining("/threads?agentId=agent-1"),
|
|
1216
|
+
expect.objectContaining({ method: "GET" }),
|
|
1217
|
+
);
|
|
1218
|
+
const nonNullContexts = getDispatchedContexts().filter(
|
|
1219
|
+
(context) => context !== null,
|
|
1220
|
+
);
|
|
1221
|
+
expect(nonNullContexts).toHaveLength(1);
|
|
1222
|
+
});
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
describe("thread store registration (core registry)", () => {
|
|
1226
|
+
it("registers the thread store with core when enabled", async () => {
|
|
1227
|
+
const copilotkit = setupCopilotKit();
|
|
1228
|
+
|
|
1229
|
+
fetchMock
|
|
1230
|
+
.mockReturnValueOnce(
|
|
1231
|
+
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
1232
|
+
)
|
|
1233
|
+
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
|
|
1234
|
+
|
|
1235
|
+
mountHook();
|
|
1236
|
+
|
|
1237
|
+
await vi.waitFor(() => {
|
|
1238
|
+
expect(copilotkit.value.registerThreadStore).toHaveBeenCalledWith(
|
|
1239
|
+
"agent-1",
|
|
1240
|
+
expect.objectContaining({ select: expect.any(Function) }),
|
|
1241
|
+
);
|
|
1242
|
+
});
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
it("does not register the thread store when disabled (avoids evicting a live store for the same agent)", async () => {
|
|
1246
|
+
const copilotkit = setupCopilotKit();
|
|
1247
|
+
|
|
1248
|
+
mountHook({ ...defaultInput, enabled: false });
|
|
1249
|
+
|
|
1250
|
+
await nextTick();
|
|
1251
|
+
|
|
1252
|
+
expect(copilotkit.value.registerThreadStore).not.toHaveBeenCalled();
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
it("unregisters the thread store with core on unmount", async () => {
|
|
1256
|
+
const copilotkit = setupCopilotKit();
|
|
1257
|
+
|
|
1258
|
+
fetchMock
|
|
1259
|
+
.mockReturnValueOnce(
|
|
1260
|
+
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
1261
|
+
)
|
|
1262
|
+
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
|
|
1263
|
+
|
|
1264
|
+
const { wrapper } = mountHook();
|
|
1265
|
+
|
|
1266
|
+
await vi.waitFor(() => {
|
|
1267
|
+
expect(copilotkit.value.registerThreadStore).toHaveBeenCalled();
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
wrapper.unmount();
|
|
1271
|
+
|
|
1272
|
+
expect(copilotkit.value.unregisterThreadStore).toHaveBeenCalledWith(
|
|
1273
|
+
"agent-1",
|
|
1274
|
+
);
|
|
1275
|
+
});
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
describe("threadEndpoints gating", () => {
|
|
1279
|
+
it("does not fetch when the runtime does not advertise the list endpoint and surfaces the endpoints-unavailable error", async () => {
|
|
1280
|
+
setupCopilotKit(
|
|
1281
|
+
"http://localhost:4000",
|
|
1282
|
+
CopilotKitCoreRuntimeConnectionStatus.Connected,
|
|
1283
|
+
{
|
|
1284
|
+
threadEndpoints: {
|
|
1285
|
+
list: false,
|
|
1286
|
+
inspect: false,
|
|
1287
|
+
mutations: false,
|
|
1288
|
+
realtimeMetadata: false,
|
|
1289
|
+
},
|
|
1290
|
+
intelligence: undefined,
|
|
1291
|
+
},
|
|
1292
|
+
);
|
|
1293
|
+
|
|
1294
|
+
const { getResult } = mountHook();
|
|
1295
|
+
|
|
1296
|
+
await vi.waitFor(() => {
|
|
1297
|
+
expect(getResult().isLoading.value).toBe(false);
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
1301
|
+
expect(getResult().threads.value).toEqual([]);
|
|
1302
|
+
const lastDispatched = getDispatchedContexts().at(-1);
|
|
1303
|
+
expect(lastDispatched).toBeNull();
|
|
1304
|
+
expect(getResult().error.value?.message).toBe(
|
|
1305
|
+
"Thread endpoints are not available on this CopilotKit runtime",
|
|
1306
|
+
);
|
|
1307
|
+
// The config error must NOT leak into the user-facing list error.
|
|
1308
|
+
expect(getResult().listError.value).toBeNull();
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
it("rejects mutations locally when the runtime reports mutations are unsupported", async () => {
|
|
1312
|
+
setupCopilotKit(
|
|
1313
|
+
"http://localhost:4000",
|
|
1314
|
+
CopilotKitCoreRuntimeConnectionStatus.Connected,
|
|
1315
|
+
{
|
|
1316
|
+
threadEndpoints: {
|
|
1317
|
+
list: true,
|
|
1318
|
+
inspect: true,
|
|
1319
|
+
mutations: false,
|
|
1320
|
+
realtimeMetadata: false,
|
|
1321
|
+
},
|
|
1322
|
+
intelligence: undefined,
|
|
1323
|
+
},
|
|
1324
|
+
);
|
|
1325
|
+
fetchMock.mockReturnValueOnce(jsonResponse({ threads: sampleThreads }));
|
|
1326
|
+
|
|
1327
|
+
const { getResult } = mountHook();
|
|
1328
|
+
|
|
1329
|
+
await vi.waitFor(() => {
|
|
1330
|
+
expect(getResult().isLoading.value).toBe(false);
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
fetchMock.mockClear();
|
|
1334
|
+
|
|
1335
|
+
await expect(getResult().renameThread("t-1", "Renamed")).rejects.toThrow(
|
|
1336
|
+
"Thread mutations are not available on this CopilotKit runtime",
|
|
1337
|
+
);
|
|
1338
|
+
await expect(getResult().archiveThread("t-1")).rejects.toThrow(
|
|
1339
|
+
"Thread mutations are not available on this CopilotKit runtime",
|
|
1340
|
+
);
|
|
1341
|
+
await expect(getResult().unarchiveThread("t-1")).rejects.toThrow(
|
|
1342
|
+
"Thread mutations are not available on this CopilotKit runtime",
|
|
1343
|
+
);
|
|
1344
|
+
await expect(getResult().deleteThread("t-1")).rejects.toThrow(
|
|
1345
|
+
"Thread mutations are not available on this CopilotKit runtime",
|
|
1346
|
+
);
|
|
1347
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
1348
|
+
});
|
|
1349
|
+
});
|
|
1041
1350
|
});
|
|
@@ -5,8 +5,10 @@ import {
|
|
|
5
5
|
ɵcreateThreadStore,
|
|
6
6
|
ɵselectHasNextPage,
|
|
7
7
|
ɵselectIsFetchingNextPage,
|
|
8
|
+
ɵselectIsMutating,
|
|
8
9
|
ɵselectThreads,
|
|
9
10
|
ɵselectThreadsError,
|
|
11
|
+
ɵselectFetchMoreError,
|
|
10
12
|
ɵselectThreadsIsLoading,
|
|
11
13
|
} from "@copilotkit/core";
|
|
12
14
|
import type { ɵThreadRuntimeContext, ɵThreadStore } from "@copilotkit/core";
|
|
@@ -32,17 +34,36 @@ export interface UseThreadsInput {
|
|
|
32
34
|
agentId: MaybeRefOrGetter<string>;
|
|
33
35
|
includeArchived?: MaybeRefOrGetter<boolean | undefined>;
|
|
34
36
|
limit?: MaybeRefOrGetter<number | undefined>;
|
|
37
|
+
/**
|
|
38
|
+
* When `false`, the hook clears the store context and issues no `/threads`
|
|
39
|
+
* request — used by the drawer to skip fetching while unlicensed. Defaults
|
|
40
|
+
* to `true`.
|
|
41
|
+
*/
|
|
42
|
+
enabled?: MaybeRefOrGetter<boolean | undefined>;
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
export interface UseThreadsResult {
|
|
38
46
|
threads: Ref<Thread[]>;
|
|
39
47
|
isLoading: Ref<boolean>;
|
|
40
48
|
error: Ref<Error | null>;
|
|
49
|
+
/** Genuine list-load/mutation errors only — excludes dev/config errors. */
|
|
50
|
+
listError: Ref<Error | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Error from the most recent FAILED next-page (fetch-more) load, or `null`.
|
|
53
|
+
* Tracked separately from {@link listError} so a paginated-load failure
|
|
54
|
+
* shows an inline "couldn't load more" affordance while the loaded list
|
|
55
|
+
* stays visible. Cleared when a fetch-more is retried or succeeds.
|
|
56
|
+
*/
|
|
57
|
+
fetchMoreError: Ref<Error | null>;
|
|
41
58
|
hasMoreThreads: Ref<boolean>;
|
|
42
59
|
isFetchingMoreThreads: Ref<boolean>;
|
|
60
|
+
isMutating: Ref<boolean>;
|
|
43
61
|
fetchMoreThreads: () => void;
|
|
62
|
+
refetchThreads: () => void;
|
|
63
|
+
startNewThread: () => void;
|
|
44
64
|
renameThread: (threadId: string, name: string) => Promise<void>;
|
|
45
65
|
archiveThread: (threadId: string) => Promise<void>;
|
|
66
|
+
unarchiveThread: (threadId: string) => Promise<void>;
|
|
46
67
|
deleteThread: (threadId: string) => Promise<void>;
|
|
47
68
|
}
|
|
48
69
|
|
|
@@ -77,6 +98,41 @@ export function useThreads(input: UseThreadsInput): UseThreadsResult {
|
|
|
77
98
|
toValue(input.includeArchived),
|
|
78
99
|
);
|
|
79
100
|
const resolvedLimit = computed(() => toValue(input.limit));
|
|
101
|
+
const resolvedEnabled = computed(() => toValue(input.enabled) ?? true);
|
|
102
|
+
|
|
103
|
+
// Thread-endpoint capability, derived from the runtime's `/info` payload.
|
|
104
|
+
// `threadEndpoints` is `undefined` on legacy runtimes that don't advertise
|
|
105
|
+
// the capability, so we treat "not explicitly false" as supported (matching
|
|
106
|
+
// React's `!== false` semantics) to preserve legacy thread behavior.
|
|
107
|
+
const threadListEndpointSupported = computed(
|
|
108
|
+
() => copilotkit.value.threadEndpoints?.list !== false,
|
|
109
|
+
);
|
|
110
|
+
const threadMutationsSupported = computed(
|
|
111
|
+
() => copilotkit.value.threadEndpoints?.mutations !== false,
|
|
112
|
+
);
|
|
113
|
+
// Connected, but the runtime explicitly does not serve the list endpoint.
|
|
114
|
+
const threadEndpointsUnavailable = computed(
|
|
115
|
+
() =>
|
|
116
|
+
!!copilotkit.value.runtimeUrl &&
|
|
117
|
+
copilotkit.value.runtimeConnectionStatus ===
|
|
118
|
+
CopilotKitCoreRuntimeConnectionStatus.Connected &&
|
|
119
|
+
!threadListEndpointSupported.value,
|
|
120
|
+
);
|
|
121
|
+
const threadEndpointsError = computed<Error | null>(() =>
|
|
122
|
+
threadEndpointsUnavailable.value
|
|
123
|
+
? new Error(
|
|
124
|
+
"Thread endpoints are not available on this CopilotKit runtime",
|
|
125
|
+
)
|
|
126
|
+
: null,
|
|
127
|
+
);
|
|
128
|
+
const threadMutationsError = computed<Error | null>(() =>
|
|
129
|
+
threadMutationsSupported.value
|
|
130
|
+
? null
|
|
131
|
+
: new Error(
|
|
132
|
+
"Thread mutations are not available on this CopilotKit runtime",
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
|
|
80
136
|
const headersKey = computed(() =>
|
|
81
137
|
JSON.stringify(
|
|
82
138
|
Object.entries(copilotkit.value.headers ?? {}).sort(([left], [right]) =>
|
|
@@ -88,24 +144,48 @@ export function useThreads(input: UseThreadsInput): UseThreadsResult {
|
|
|
88
144
|
const threads = ref<Thread[]>([]);
|
|
89
145
|
const storeIsLoading = ref(false);
|
|
90
146
|
const storeError = ref<Error | null>(null);
|
|
147
|
+
const fetchMoreError = ref<Error | null>(null);
|
|
91
148
|
const hasMoreThreads = ref(false);
|
|
92
149
|
const isFetchingMoreThreads = ref(false);
|
|
150
|
+
const isMutating = ref(false);
|
|
93
151
|
|
|
94
152
|
bindThreadStoreSelector(store, ɵselectThreads, threads as Ref<Thread[]>);
|
|
95
153
|
bindThreadStoreSelector(store, ɵselectThreadsIsLoading, storeIsLoading);
|
|
96
154
|
bindThreadStoreSelector(store, ɵselectThreadsError, storeError);
|
|
155
|
+
bindThreadStoreSelector(store, ɵselectFetchMoreError, fetchMoreError);
|
|
97
156
|
bindThreadStoreSelector(store, ɵselectHasNextPage, hasMoreThreads);
|
|
98
157
|
bindThreadStoreSelector(
|
|
99
158
|
store,
|
|
100
159
|
ɵselectIsFetchingNextPage,
|
|
101
160
|
isFetchingMoreThreads,
|
|
102
161
|
);
|
|
162
|
+
bindThreadStoreSelector(store, ɵselectIsMutating, isMutating);
|
|
103
163
|
|
|
104
164
|
store.start();
|
|
105
165
|
onScopeDispose(() => {
|
|
106
166
|
store.stop();
|
|
107
167
|
});
|
|
108
168
|
|
|
169
|
+
// Register this store in core's single-slot thread-store registry so other
|
|
170
|
+
// surfaces (e.g. the chat view) can resolve the same store for the agent.
|
|
171
|
+
//
|
|
172
|
+
// Gated on `resolvedEnabled`: a disabled (e.g. unlicensed) drawer must not
|
|
173
|
+
// claim the agentId slot. The registry is single-slot/last-writer-wins, so
|
|
174
|
+
// registering an inert store would evict — and on unmount tear down — a
|
|
175
|
+
// co-mounted live store for the same agent. Staying unregistered while
|
|
176
|
+
// disabled leaves the live store's registration intact.
|
|
177
|
+
watch(
|
|
178
|
+
[resolvedEnabled, resolvedAgentId],
|
|
179
|
+
([enabled, agentId], _prev, onCleanup) => {
|
|
180
|
+
if (!enabled) return;
|
|
181
|
+
copilotkit.value.registerThreadStore(agentId, store);
|
|
182
|
+
onCleanup(() => {
|
|
183
|
+
copilotkit.value.unregisterThreadStore(agentId);
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
{ immediate: true },
|
|
187
|
+
);
|
|
188
|
+
|
|
109
189
|
// Tracks whether we've dispatched the first real context to the store.
|
|
110
190
|
// The store itself starts with `isLoading: false`, so before we dispatch
|
|
111
191
|
// consumers would otherwise see an empty, non-loading state (empty-list
|
|
@@ -135,10 +215,23 @@ export function useThreads(input: UseThreadsInput): UseThreadsResult {
|
|
|
135
215
|
resolvedAgentId,
|
|
136
216
|
resolvedIncludeArchived,
|
|
137
217
|
resolvedLimit,
|
|
218
|
+
resolvedEnabled,
|
|
219
|
+
threadListEndpointSupported,
|
|
138
220
|
],
|
|
139
|
-
([
|
|
140
|
-
|
|
221
|
+
([
|
|
222
|
+
runtimeUrl,
|
|
223
|
+
runtimeStatus,
|
|
224
|
+
,
|
|
225
|
+
wsUrl,
|
|
226
|
+
agentId,
|
|
227
|
+
includeArchived,
|
|
228
|
+
limit,
|
|
229
|
+
enabled,
|
|
230
|
+
listEndpointSupported,
|
|
231
|
+
]) => {
|
|
232
|
+
if (!runtimeUrl || !enabled) {
|
|
141
233
|
store.setContext(null);
|
|
234
|
+
hasDispatchedContext.value = false;
|
|
142
235
|
return;
|
|
143
236
|
}
|
|
144
237
|
|
|
@@ -146,6 +239,15 @@ export function useThreads(input: UseThreadsInput): UseThreadsResult {
|
|
|
146
239
|
return;
|
|
147
240
|
}
|
|
148
241
|
|
|
242
|
+
// Connected, but the runtime explicitly does not serve the list
|
|
243
|
+
// endpoint: stay inert so no `/threads` fetch fires. The
|
|
244
|
+
// `threadEndpointsError` surfaces via `error` instead.
|
|
245
|
+
if (!listEndpointSupported) {
|
|
246
|
+
store.setContext(null);
|
|
247
|
+
hasDispatchedContext.value = false;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
149
251
|
const context: ɵThreadRuntimeContext = {
|
|
150
252
|
runtimeUrl,
|
|
151
253
|
headers: { ...copilotkit.value.headers },
|
|
@@ -168,19 +270,44 @@ export function useThreads(input: UseThreadsInput): UseThreadsResult {
|
|
|
168
270
|
);
|
|
169
271
|
|
|
170
272
|
const preConnectLoading = computed(
|
|
171
|
-
() =>
|
|
273
|
+
() =>
|
|
274
|
+
!!copilotkit.value.runtimeUrl &&
|
|
275
|
+
resolvedEnabled.value &&
|
|
276
|
+
!threadEndpointsUnavailable.value &&
|
|
277
|
+
!hasDispatchedContext.value,
|
|
172
278
|
);
|
|
173
279
|
|
|
174
280
|
const isLoading = computed(() =>
|
|
175
|
-
runtimeError.value
|
|
281
|
+
runtimeError.value || threadEndpointsError.value
|
|
176
282
|
? false
|
|
177
283
|
: preConnectLoading.value || storeIsLoading.value,
|
|
178
284
|
);
|
|
179
285
|
|
|
286
|
+
// Folds developer/config errors (missing runtime URL, runtime without
|
|
287
|
+
// thread endpoints) together with genuine store errors. `listError` below
|
|
288
|
+
// deliberately excludes the config errors.
|
|
180
289
|
const error = computed<Error | null>(
|
|
181
|
-
() => runtimeError.value ?? storeError.value,
|
|
290
|
+
() => runtimeError.value ?? threadEndpointsError.value ?? storeError.value,
|
|
182
291
|
);
|
|
183
292
|
|
|
293
|
+
// Genuine store errors only. Unlike `error`, this omits the dev/config
|
|
294
|
+
// `runtimeError`/`threadEndpointsError` strings so they are never shown in
|
|
295
|
+
// user-facing error UI.
|
|
296
|
+
const listError = computed<Error | null>(() => storeError.value);
|
|
297
|
+
|
|
298
|
+
// Reject mutations locally (before touching the network) when the runtime
|
|
299
|
+
// reports mutations are unsupported, matching React's `guardMutation`.
|
|
300
|
+
function guardMutation<TArgs extends unknown[]>(
|
|
301
|
+
mutation: (...args: TArgs) => Promise<void>,
|
|
302
|
+
): (...args: TArgs) => Promise<void> {
|
|
303
|
+
return (...args: TArgs) => {
|
|
304
|
+
if (threadMutationsError.value) {
|
|
305
|
+
return Promise.reject(threadMutationsError.value);
|
|
306
|
+
}
|
|
307
|
+
return mutation(...args);
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
184
311
|
return {
|
|
185
312
|
threads: computed(() =>
|
|
186
313
|
threads.value.map(
|
|
@@ -197,12 +324,25 @@ export function useThreads(input: UseThreadsInput): UseThreadsResult {
|
|
|
197
324
|
),
|
|
198
325
|
isLoading,
|
|
199
326
|
error,
|
|
327
|
+
listError,
|
|
328
|
+
fetchMoreError,
|
|
200
329
|
hasMoreThreads,
|
|
201
330
|
isFetchingMoreThreads,
|
|
331
|
+
isMutating,
|
|
202
332
|
fetchMoreThreads: () => store.fetchNextPage(),
|
|
203
|
-
|
|
333
|
+
refetchThreads: () => store.refetchThreads(),
|
|
334
|
+
startNewThread: () => store.startNewThread(),
|
|
335
|
+
renameThread: guardMutation((threadId: string, name: string) =>
|
|
204
336
|
store.renameThread(threadId, name),
|
|
205
|
-
|
|
206
|
-
|
|
337
|
+
),
|
|
338
|
+
archiveThread: guardMutation((threadId: string) =>
|
|
339
|
+
store.archiveThread(threadId),
|
|
340
|
+
),
|
|
341
|
+
unarchiveThread: guardMutation((threadId: string) =>
|
|
342
|
+
store.unarchiveThread(threadId),
|
|
343
|
+
),
|
|
344
|
+
deleteThread: guardMutation((threadId: string) =>
|
|
345
|
+
store.deleteThread(threadId),
|
|
346
|
+
),
|
|
207
347
|
};
|
|
208
348
|
}
|