@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.
Files changed (48) hide show
  1. package/dist/index.cjs +1 -1
  2. package/dist/index.mjs +70 -69
  3. package/dist/styles.css +1 -1
  4. package/dist/use-render-activity-message-CIvEoQzA.cjs +85 -0
  5. package/dist/use-render-activity-message-CIvEoQzA.cjs.map +1 -0
  6. package/dist/{use-render-activity-message-8_HyVZWc.js → use-render-activity-message-CbKeqosY.js} +3514 -3207
  7. package/dist/use-render-activity-message-CbKeqosY.js.map +1 -0
  8. package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
  9. package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts +8 -0
  10. package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts.map +1 -1
  11. package/dist/v2/components/chat/CopilotModalHeader.vue.d.ts.map +1 -1
  12. package/dist/v2/components/chat/CopilotThreadsDrawer.vue.d.ts +65 -0
  13. package/dist/v2/components/chat/CopilotThreadsDrawer.vue.d.ts.map +1 -0
  14. package/dist/v2/components/chat/index.d.ts +1 -0
  15. package/dist/v2/components/chat/index.d.ts.map +1 -1
  16. package/dist/v2/components/icons/index.d.ts +1 -1
  17. package/dist/v2/components/icons/index.d.ts.map +1 -1
  18. package/dist/v2/hooks/use-threads.d.ts +19 -0
  19. package/dist/v2/hooks/use-threads.d.ts.map +1 -1
  20. package/dist/v2/index.cjs +1 -1
  21. package/dist/v2/index.mjs +44 -43
  22. package/dist/v2/lib/is-mobile-viewport.d.ts +15 -0
  23. package/dist/v2/lib/is-mobile-viewport.d.ts.map +1 -0
  24. package/dist/v2/providers/CopilotChatConfigurationProvider.vue.d.ts.map +1 -1
  25. package/dist/v2/providers/types.d.ts +24 -0
  26. package/dist/v2/providers/types.d.ts.map +1 -1
  27. package/package.json +5 -4
  28. package/src/v2/components/chat/CopilotChat.vue +22 -4
  29. package/src/v2/components/chat/CopilotModalHeader.vue +61 -2
  30. package/src/v2/components/chat/CopilotThreadsDrawer.vue +335 -0
  31. package/src/v2/components/chat/__tests__/CopilotChat.clearOnFresh.test.ts +160 -0
  32. package/src/v2/components/chat/__tests__/CopilotModalHeader.drawerLauncher.test.ts +125 -0
  33. package/src/v2/components/chat/__tests__/CopilotThreadsDrawer.ssr.test.ts +25 -0
  34. package/src/v2/components/chat/__tests__/CopilotThreadsDrawer.test.ts +910 -0
  35. package/src/v2/components/chat/index.ts +1 -0
  36. package/src/v2/components/icons/index.ts +1 -0
  37. package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.ts +4 -4
  38. package/src/v2/hooks/__tests__/use-threads.test.ts +315 -6
  39. package/src/v2/hooks/use-threads.ts +148 -8
  40. package/src/v2/lib/__tests__/is-mobile-viewport.test.ts +48 -0
  41. package/src/v2/lib/is-mobile-viewport.ts +23 -0
  42. package/src/v2/providers/CopilotChatConfigurationProvider.vue +120 -4
  43. package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.ts +105 -1
  44. package/src/v2/providers/types.ts +25 -0
  45. package/vite.config.ts +7 -0
  46. package/dist/use-render-activity-message-8_HyVZWc.js.map +0 -1
  47. package/dist/use-render-activity-message-BqgmNPCz.cjs +0 -85
  48. 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";
@@ -19,4 +19,5 @@ export {
19
19
  RefreshCw as IconRefreshCw,
20
20
  CheckCircle as IconCheckCircle,
21
21
  Circle as IconCircle,
22
+ PanelLeftOpen as IconPanelLeftOpen,
22
23
  } from "lucide-vue-next";
@@ -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).toBe(
949
- ToolCallStatus.Executing,
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
- ([runtimeUrl, runtimeStatus, , wsUrl, agentId, includeArchived, limit]) => {
140
- if (!runtimeUrl) {
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
- () => !!copilotkit.value.runtimeUrl && !hasDispatchedContext.value,
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
- renameThread: (threadId: string, name: string) =>
333
+ refetchThreads: () => store.refetchThreads(),
334
+ startNewThread: () => store.startNewThread(),
335
+ renameThread: guardMutation((threadId: string, name: string) =>
204
336
  store.renameThread(threadId, name),
205
- archiveThread: (threadId: string) => store.archiveThread(threadId),
206
- deleteThread: (threadId: string) => store.deleteThread(threadId),
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
  }