@copilotkit/react-core 1.56.4 → 1.56.5-canary.1777671752
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/{copilotkit-Bd0m5HFp.mjs → copilotkit-CPe2-340.mjs} +130 -80
- package/dist/copilotkit-CPe2-340.mjs.map +1 -0
- package/dist/copilotkit-DFaI4j2r.d.mts.map +1 -1
- package/dist/{copilotkit-tb4zqaMK.cjs → copilotkit-DGbvw8n2.cjs} +130 -80
- package/dist/copilotkit-DGbvw8n2.cjs.map +1 -0
- package/dist/copilotkit-Dg4r4Gi_.d.cts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.umd.js +24 -0
- package/dist/index.umd.js.map +1 -1
- package/dist/v2/index.cjs +1 -1
- package/dist/v2/index.css +1 -1
- package/dist/v2/index.mjs +1 -1
- package/dist/v2/index.umd.js +132 -82
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +8 -8
- package/src/v2/components/chat/CopilotChatAttachmentQueue.tsx +3 -113
- package/src/v2/components/chat/CopilotChatAttachmentRenderer.tsx +26 -6
- package/src/v2/components/chat/CopilotChatUserMessage.tsx +2 -2
- package/src/v2/components/chat/CopilotChatView.tsx +21 -5
- package/src/v2/components/chat/Lightbox.tsx +103 -0
- package/src/v2/components/chat/__tests__/CopilotChat.suggestionsAlways.test.tsx +183 -0
- package/src/v2/components/chat/__tests__/CopilotChatView.inputOverlay.test.tsx +92 -0
- package/src/v2/hooks/__tests__/use-threads.test.tsx +229 -27
- package/src/v2/hooks/use-configure-suggestions.tsx +50 -1
- package/src/v2/hooks/use-threads.tsx +7 -1
- package/dist/copilotkit-Bd0m5HFp.mjs.map +0 -1
- package/dist/copilotkit-tb4zqaMK.cjs.map +0 -1
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
3
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { useCopilotKit } from "../../providers/CopilotKitProvider";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
CopilotKitCoreRuntimeConnectionStatus,
|
|
7
|
+
ɵMAX_SOCKET_RETRIES,
|
|
8
|
+
} from "@copilotkit/core";
|
|
6
9
|
|
|
7
10
|
vi.mock("../../providers/CopilotKitProvider", () => ({
|
|
8
11
|
useCopilotKit: vi.fn(),
|
|
9
12
|
}));
|
|
10
13
|
|
|
11
14
|
const mockUseCopilotKit = useCopilotKit as ReturnType<typeof vi.fn>;
|
|
15
|
+
|
|
16
|
+
// Shape of the mock socket exposed via the hoisted `phoenix.sockets`
|
|
17
|
+
// array. Defined as a named type so test-side assertions can drop the
|
|
18
|
+
// blanket `any[]` cast and surface socket-API typos at compile time.
|
|
19
|
+
interface MockChannelLike {
|
|
20
|
+
topic: string;
|
|
21
|
+
params: Record<string, unknown>;
|
|
22
|
+
left: boolean;
|
|
23
|
+
serverPush(event: string, payload: unknown): void;
|
|
24
|
+
}
|
|
25
|
+
interface MockSocketLike {
|
|
26
|
+
url: string;
|
|
27
|
+
connected: boolean;
|
|
28
|
+
disconnected: boolean;
|
|
29
|
+
channels: MockChannelLike[];
|
|
30
|
+
triggerError(error?: unknown): void;
|
|
31
|
+
triggerOpen(): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
12
34
|
const phoenix = vi.hoisted(() => ({
|
|
13
|
-
sockets: [] as
|
|
35
|
+
sockets: [] as MockSocketLike[],
|
|
14
36
|
}));
|
|
15
37
|
|
|
16
38
|
vi.mock("phoenix", () => {
|
|
@@ -31,13 +53,11 @@ vi.mock("phoenix", () => {
|
|
|
31
53
|
topic: string;
|
|
32
54
|
params: Record<string, unknown>;
|
|
33
55
|
left = false;
|
|
34
|
-
channels: MockChannel[] = [];
|
|
35
56
|
|
|
36
57
|
private handlers = new Map<
|
|
37
58
|
string,
|
|
38
59
|
Array<{ ref: number; callback: (payload: unknown) => void }>
|
|
39
60
|
>();
|
|
40
|
-
private joinPush = new MockPush();
|
|
41
61
|
private nextRef = 1;
|
|
42
62
|
|
|
43
63
|
constructor(topic = "", params: Record<string, unknown> = {}) {
|
|
@@ -63,14 +83,24 @@ vi.mock("phoenix", () => {
|
|
|
63
83
|
return;
|
|
64
84
|
}
|
|
65
85
|
|
|
86
|
+
// Re-check after the early returns above: `off(event)` deletes the
|
|
87
|
+
// entry, so a subsequent `off(event, ref)` would otherwise hit a
|
|
88
|
+
// non-null assertion that lies and `.filter` on undefined.
|
|
89
|
+
const entries = this.handlers.get(event);
|
|
90
|
+
if (entries === undefined) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
66
93
|
this.handlers.set(
|
|
67
94
|
event,
|
|
68
|
-
|
|
95
|
+
entries.filter((entry) => entry.ref !== ref),
|
|
69
96
|
);
|
|
70
97
|
}
|
|
71
98
|
|
|
72
99
|
join(): MockPush {
|
|
73
|
-
|
|
100
|
+
// Each rejoin must produce a distinct push instance — sharing
|
|
101
|
+
// one across joins lets stale "ok"/"error" callbacks from a
|
|
102
|
+
// prior join fire against a new join's listeners.
|
|
103
|
+
return new MockPush();
|
|
74
104
|
}
|
|
75
105
|
|
|
76
106
|
leave(): void {
|
|
@@ -101,10 +131,22 @@ vi.mock("phoenix", () => {
|
|
|
101
131
|
}
|
|
102
132
|
|
|
103
133
|
connect(): void {
|
|
134
|
+
// Phoenix sockets fire `onOpen` exactly once per WebSocket upgrade,
|
|
135
|
+
// and the upgrade is asynchronous. Tests must drive that transition
|
|
136
|
+
// explicitly via `triggerOpen()` so we exercise one open per
|
|
137
|
+
// connection — auto-firing here would either double-fire (when a
|
|
138
|
+
// test also calls `triggerOpen()`) or hide cases where production
|
|
139
|
+
// code forgets to await the open before joining a channel.
|
|
104
140
|
this.connected = true;
|
|
105
141
|
}
|
|
106
142
|
|
|
107
143
|
disconnect(): void {
|
|
144
|
+
// Real Phoenix sockets flip `connected` back to false on disconnect —
|
|
145
|
+
// a mock that only sets `disconnected = true` lets a regression that
|
|
146
|
+
// forgets to clear `connected` slip through, since assertions like
|
|
147
|
+
// `socket.connected === false` would be vacuously satisfied by the
|
|
148
|
+
// initial value but never re-checked after a reconnect cycle.
|
|
149
|
+
this.connected = false;
|
|
108
150
|
this.disconnected = true;
|
|
109
151
|
}
|
|
110
152
|
|
|
@@ -139,9 +181,12 @@ vi.mock("phoenix", () => {
|
|
|
139
181
|
});
|
|
140
182
|
|
|
141
183
|
const fetchMock = vi.fn();
|
|
142
|
-
|
|
184
|
+
// Use `vi.stubGlobal` so the original `fetch` is restored automatically
|
|
185
|
+
// by `vi.unstubAllGlobals()` below — direct `globalThis.fetch = ...`
|
|
186
|
+
// assignment leaks the mock across test files in the same vitest worker.
|
|
187
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
143
188
|
|
|
144
|
-
function getMockSockets():
|
|
189
|
+
function getMockSockets(): MockSocketLike[] {
|
|
145
190
|
return phoenix.sockets;
|
|
146
191
|
}
|
|
147
192
|
|
|
@@ -154,6 +199,8 @@ function setupCopilotKit(runtimeUrl = "http://localhost:4000") {
|
|
|
154
199
|
intelligence: {
|
|
155
200
|
wsUrl: "ws://localhost:4000/client",
|
|
156
201
|
},
|
|
202
|
+
registerThreadStore: vi.fn(),
|
|
203
|
+
unregisterThreadStore: vi.fn(),
|
|
157
204
|
},
|
|
158
205
|
});
|
|
159
206
|
}
|
|
@@ -198,9 +245,20 @@ describe("useThreads", () => {
|
|
|
198
245
|
beforeEach(() => {
|
|
199
246
|
phoenix.sockets.splice(0);
|
|
200
247
|
fetchMock.mockReset();
|
|
248
|
+
// Reset before re-priming. setupCopilotKit() uses mockReturnValue, so a
|
|
249
|
+
// future test that uses mockReturnValueOnce would otherwise leak any
|
|
250
|
+
// un-consumed queued returns into the next test.
|
|
251
|
+
mockUseCopilotKit.mockReset();
|
|
201
252
|
setupCopilotKit();
|
|
202
253
|
});
|
|
203
254
|
|
|
255
|
+
afterAll(() => {
|
|
256
|
+
// Pair with `vi.stubGlobal("fetch", fetchMock)` above. Without this
|
|
257
|
+
// restoration the mock leaks into any sibling test file that runs in
|
|
258
|
+
// the same vitest worker and assumes a real `fetch`.
|
|
259
|
+
vi.unstubAllGlobals();
|
|
260
|
+
});
|
|
261
|
+
|
|
204
262
|
it("fetches threads and subscribes to the user metadata channel", async () => {
|
|
205
263
|
fetchMock
|
|
206
264
|
.mockReturnValueOnce(
|
|
@@ -323,6 +381,9 @@ describe("useThreads", () => {
|
|
|
323
381
|
await waitFor(() => {
|
|
324
382
|
expect(result.current.threads).toHaveLength(1);
|
|
325
383
|
});
|
|
384
|
+
// Identity-check the remaining thread so a regression that removes
|
|
385
|
+
// the wrong thread (e.g. a swapped index) is caught.
|
|
386
|
+
expect(result.current.threads[0].id).toBe("t-1");
|
|
326
387
|
});
|
|
327
388
|
|
|
328
389
|
it("renames a thread through the runtime contract", async () => {
|
|
@@ -343,10 +404,18 @@ describe("useThreads", () => {
|
|
|
343
404
|
await result.current.renameThread("t-1", "Renamed");
|
|
344
405
|
});
|
|
345
406
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
407
|
+
// Find the PATCH call by URL+method rather than a hardcoded index —
|
|
408
|
+
// a future change to the fetch order (or an extra startup fetch) must
|
|
409
|
+
// not silently miss the actual rename request.
|
|
410
|
+
const renameCall = fetchMock.mock.calls.find(
|
|
411
|
+
(args: unknown[]) =>
|
|
412
|
+
typeof args[0] === "string" &&
|
|
413
|
+
(args[0] as string).includes("/threads/t-1") &&
|
|
414
|
+
(args[1] as { method?: string } | undefined)?.method === "PATCH",
|
|
415
|
+
);
|
|
416
|
+
expect(renameCall).toBeDefined();
|
|
417
|
+
const [, renameOptions] = renameCall!;
|
|
418
|
+
expect(JSON.parse((renameOptions as { body: string }).body)).toMatchObject({
|
|
350
419
|
agentId: "agent-1",
|
|
351
420
|
name: "Renamed",
|
|
352
421
|
});
|
|
@@ -372,17 +441,31 @@ describe("useThreads", () => {
|
|
|
372
441
|
await result.current.deleteThread("t-1");
|
|
373
442
|
});
|
|
374
443
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
444
|
+
// Filter by URL+method rather than fixed indices (mirrors the rename
|
|
445
|
+
// test above). A future change to the startup fetch order — adding an
|
|
446
|
+
// /info call, splitting the join token, etc. — must not silently miss
|
|
447
|
+
// the actual archive/delete requests.
|
|
448
|
+
const archiveCall = fetchMock.mock.calls.find(
|
|
449
|
+
(args: unknown[]) =>
|
|
450
|
+
typeof args[0] === "string" &&
|
|
451
|
+
(args[0] as string).includes("/threads/t-2/archive") &&
|
|
452
|
+
(args[1] as { method?: string } | undefined)?.method === "POST",
|
|
453
|
+
);
|
|
454
|
+
expect(archiveCall).toBeDefined();
|
|
455
|
+
expect(
|
|
456
|
+
JSON.parse((archiveCall![1] as { body: string }).body),
|
|
457
|
+
).toMatchObject({ agentId: "agent-1" });
|
|
458
|
+
|
|
459
|
+
const deleteCall = fetchMock.mock.calls.find(
|
|
460
|
+
(args: unknown[]) =>
|
|
461
|
+
typeof args[0] === "string" &&
|
|
462
|
+
(args[0] as string).includes("/threads/t-1") &&
|
|
463
|
+
(args[1] as { method?: string } | undefined)?.method === "DELETE",
|
|
464
|
+
);
|
|
465
|
+
expect(deleteCall).toBeDefined();
|
|
466
|
+
expect(JSON.parse((deleteCall![1] as { body: string }).body)).toMatchObject(
|
|
467
|
+
{ agentId: "agent-1" },
|
|
468
|
+
);
|
|
386
469
|
});
|
|
387
470
|
|
|
388
471
|
it("exposes thread-scoped pagination properties", async () => {
|
|
@@ -414,6 +497,63 @@ describe("useThreads", () => {
|
|
|
414
497
|
expect(typeof result.current.fetchMoreThreads).toBe("function");
|
|
415
498
|
});
|
|
416
499
|
|
|
500
|
+
it("fetchMoreThreads fetches the next page with the cursor and appends threads", async () => {
|
|
501
|
+
const nextPageThreads = [
|
|
502
|
+
{
|
|
503
|
+
id: "t-3",
|
|
504
|
+
organizationId: "org-1",
|
|
505
|
+
agentId: "agent-1",
|
|
506
|
+
createdById: "user-1",
|
|
507
|
+
name: "Thread Three",
|
|
508
|
+
archived: false,
|
|
509
|
+
createdAt: "2026-01-03T00:00:00Z",
|
|
510
|
+
updatedAt: "2026-01-03T00:00:00Z",
|
|
511
|
+
},
|
|
512
|
+
];
|
|
513
|
+
|
|
514
|
+
fetchMock
|
|
515
|
+
.mockReturnValueOnce(
|
|
516
|
+
jsonResponse({
|
|
517
|
+
threads: sampleThreads,
|
|
518
|
+
joinCode: "jc-1",
|
|
519
|
+
nextCursor: "cursor-abc",
|
|
520
|
+
}),
|
|
521
|
+
)
|
|
522
|
+
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }))
|
|
523
|
+
.mockReturnValueOnce(
|
|
524
|
+
jsonResponse({ threads: nextPageThreads, joinCode: "jc-1" }),
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
const { result } = renderHook(() => useThreads(defaultInput));
|
|
528
|
+
|
|
529
|
+
await waitFor(() => {
|
|
530
|
+
expect(result.current.isLoading).toBe(false);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
expect(result.current.threads).toHaveLength(2);
|
|
534
|
+
expect(result.current.hasMoreThreads).toBe(true);
|
|
535
|
+
|
|
536
|
+
act(() => {
|
|
537
|
+
result.current.fetchMoreThreads();
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
await waitFor(() => {
|
|
541
|
+
expect(result.current.threads).toHaveLength(3);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
const nextPageCall = fetchMock.mock.calls.find(
|
|
545
|
+
(args: unknown[]) =>
|
|
546
|
+
typeof args[0] === "string" &&
|
|
547
|
+
(args[0] as string).includes("cursor=cursor-abc"),
|
|
548
|
+
);
|
|
549
|
+
expect(nextPageCall).toBeDefined();
|
|
550
|
+
expect(nextPageCall![0]).toContain("agentId=agent-1");
|
|
551
|
+
expect(result.current.threads.map((t: { id: string }) => t.id)).toContain(
|
|
552
|
+
"t-3",
|
|
553
|
+
);
|
|
554
|
+
expect(result.current.threads).toHaveLength(3);
|
|
555
|
+
});
|
|
556
|
+
|
|
417
557
|
it("does not expose organizationId or createdById on threads", async () => {
|
|
418
558
|
fetchMock
|
|
419
559
|
.mockReturnValueOnce(
|
|
@@ -455,10 +595,20 @@ describe("useThreads", () => {
|
|
|
455
595
|
const socket = getMockSockets()[0];
|
|
456
596
|
const channel = socket.channels[0];
|
|
457
597
|
|
|
598
|
+
// Threshold is sourced from production (ɵMAX_SOCKET_RETRIES) so a
|
|
599
|
+
// future change to the retry budget cannot silently desync the test.
|
|
600
|
+
// We fire all errors inside a single act to keep the rxjs cleanup
|
|
601
|
+
// synchronous with the assertions, then check the pre-threshold and
|
|
602
|
+
// post-threshold states by inspecting the socket between iterations.
|
|
458
603
|
act(() => {
|
|
459
|
-
for (let index = 0; index <
|
|
604
|
+
for (let index = 0; index < ɵMAX_SOCKET_RETRIES - 1; index += 1) {
|
|
460
605
|
socket.triggerError();
|
|
461
606
|
}
|
|
607
|
+
// Pre-threshold: teardown must NOT be premature.
|
|
608
|
+
expect(channel.left).toBe(false);
|
|
609
|
+
expect(socket.disconnected).toBe(false);
|
|
610
|
+
// The Nth error crosses the threshold and triggers teardown.
|
|
611
|
+
socket.triggerError();
|
|
462
612
|
});
|
|
463
613
|
|
|
464
614
|
expect(channel.left).toBe(true);
|
|
@@ -487,6 +637,47 @@ describe("useThreads", () => {
|
|
|
487
637
|
expect(socket.disconnected).toBe(true);
|
|
488
638
|
});
|
|
489
639
|
|
|
640
|
+
it("registers thread store on mount and unregisters on unmount", async () => {
|
|
641
|
+
const registerThreadStore = vi.fn();
|
|
642
|
+
const unregisterThreadStore = vi.fn();
|
|
643
|
+
// Use mockReturnValue (not mockReturnValueOnce) so the same spies are
|
|
644
|
+
// returned across all renders, including the cleanup render where
|
|
645
|
+
// unmount triggers the effect's cleanup function.
|
|
646
|
+
// runtimeConnectionStatus is set explicitly to Connected — the hook
|
|
647
|
+
// treats anything other than Connected as "do not dispatch context",
|
|
648
|
+
// and we want this test to exercise a fully-wired flow.
|
|
649
|
+
mockUseCopilotKit.mockReturnValue({
|
|
650
|
+
copilotkit: {
|
|
651
|
+
runtimeUrl: "http://localhost:4000",
|
|
652
|
+
runtimeConnectionStatus:
|
|
653
|
+
CopilotKitCoreRuntimeConnectionStatus.Connected,
|
|
654
|
+
headers: { Authorization: "Bearer test-token" },
|
|
655
|
+
intelligence: { wsUrl: "ws://localhost:4000/client" },
|
|
656
|
+
registerThreadStore,
|
|
657
|
+
unregisterThreadStore,
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
fetchMock
|
|
662
|
+
.mockReturnValueOnce(
|
|
663
|
+
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
664
|
+
)
|
|
665
|
+
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
|
|
666
|
+
|
|
667
|
+
const { unmount } = renderHook(() => useThreads(defaultInput));
|
|
668
|
+
|
|
669
|
+
await waitFor(() => {
|
|
670
|
+
expect(registerThreadStore).toHaveBeenCalledWith(
|
|
671
|
+
"agent-1",
|
|
672
|
+
expect.objectContaining({ select: expect.any(Function) }),
|
|
673
|
+
);
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
unmount();
|
|
677
|
+
|
|
678
|
+
expect(unregisterThreadStore).toHaveBeenCalledWith("agent-1");
|
|
679
|
+
});
|
|
680
|
+
|
|
490
681
|
it("waits for runtimeConnectionStatus=Connected before fetching /threads", async () => {
|
|
491
682
|
// Start in Connecting — hook should hold off on dispatching any request
|
|
492
683
|
// so the initial list fetch includes wsUrl and avoids a redundant second
|
|
@@ -498,6 +689,8 @@ describe("useThreads", () => {
|
|
|
498
689
|
CopilotKitCoreRuntimeConnectionStatus.Connecting,
|
|
499
690
|
headers: { Authorization: "Bearer test-token" },
|
|
500
691
|
intelligence: undefined,
|
|
692
|
+
registerThreadStore: vi.fn(),
|
|
693
|
+
unregisterThreadStore: vi.fn(),
|
|
501
694
|
},
|
|
502
695
|
});
|
|
503
696
|
|
|
@@ -509,8 +702,15 @@ describe("useThreads", () => {
|
|
|
509
702
|
|
|
510
703
|
const { result, rerender } = renderHook(() => useThreads(defaultInput));
|
|
511
704
|
|
|
512
|
-
//
|
|
513
|
-
|
|
705
|
+
// Flush React effects + microtasks deterministically. A bare
|
|
706
|
+
// setTimeout(20) raced the store-effect under load on slow machines.
|
|
707
|
+
// Chained microtask flushes inside `act` give every queued effect a
|
|
708
|
+
// chance to run without depending on real-time delay.
|
|
709
|
+
await act(async () => {
|
|
710
|
+
await Promise.resolve();
|
|
711
|
+
await Promise.resolve();
|
|
712
|
+
await Promise.resolve();
|
|
713
|
+
});
|
|
514
714
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
515
715
|
|
|
516
716
|
// While waiting for Connected, the hook must surface isLoading=true so
|
|
@@ -529,6 +729,8 @@ describe("useThreads", () => {
|
|
|
529
729
|
CopilotKitCoreRuntimeConnectionStatus.Connected,
|
|
530
730
|
headers: { Authorization: "Bearer test-token" },
|
|
531
731
|
intelligence: { wsUrl: "ws://localhost:4000/client" },
|
|
732
|
+
registerThreadStore: vi.fn(),
|
|
733
|
+
unregisterThreadStore: vi.fn(),
|
|
532
734
|
},
|
|
533
735
|
});
|
|
534
736
|
|
|
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
|
2
2
|
import { useCopilotKit } from "../providers/CopilotKitProvider";
|
|
3
3
|
import { useCopilotChatConfiguration } from "../providers/CopilotChatConfigurationProvider";
|
|
4
4
|
import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
|
|
5
|
-
import {
|
|
5
|
+
import type {
|
|
6
6
|
DynamicSuggestionsConfig,
|
|
7
7
|
StaticSuggestionsConfig,
|
|
8
8
|
SuggestionsConfig,
|
|
@@ -106,22 +106,37 @@ export function useConfigureSuggestions(
|
|
|
106
106
|
const isGlobalConfig =
|
|
107
107
|
rawConsumerAgentId === undefined || rawConsumerAgentId === "*";
|
|
108
108
|
|
|
109
|
+
const isDynamicConfigType = useMemo(
|
|
110
|
+
() => !!normalizedConfig && "instructions" in normalizedConfig,
|
|
111
|
+
[normalizedConfig],
|
|
112
|
+
);
|
|
113
|
+
|
|
109
114
|
const requestReload = useCallback(() => {
|
|
110
115
|
if (!normalizedConfig) {
|
|
111
116
|
return;
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
if (isGlobalConfig) {
|
|
120
|
+
const seen = new Set<string>();
|
|
115
121
|
const agents = Object.values(copilotkit.agents ?? {});
|
|
116
122
|
for (const entry of agents) {
|
|
117
123
|
const agentId = entry.agentId;
|
|
118
124
|
if (!agentId) {
|
|
119
125
|
continue;
|
|
120
126
|
}
|
|
127
|
+
seen.add(agentId);
|
|
121
128
|
if (!entry.isRunning) {
|
|
122
129
|
copilotkit.reloadSuggestions(agentId);
|
|
123
130
|
}
|
|
124
131
|
}
|
|
132
|
+
// Also reload for the chat's resolved consumer agent. The registry can
|
|
133
|
+
// be empty at this point (e.g. runtime info still loading), in which
|
|
134
|
+
// case the loop above wouldn't have fired for the agent the user is
|
|
135
|
+
// actually chatting with — and the welcome screen would render with
|
|
136
|
+
// no suggestions until they navigate away and back.
|
|
137
|
+
if (targetAgentId && !seen.has(targetAgentId)) {
|
|
138
|
+
copilotkit.reloadSuggestions(targetAgentId);
|
|
139
|
+
}
|
|
125
140
|
return;
|
|
126
141
|
}
|
|
127
142
|
|
|
@@ -169,6 +184,40 @@ export function useConfigureSuggestions(
|
|
|
169
184
|
}
|
|
170
185
|
requestReload();
|
|
171
186
|
}, [extraDeps.length, normalizedConfig, requestReload, ...extraDeps]);
|
|
187
|
+
|
|
188
|
+
// When agents arrive after the initial render (runtime info just landed),
|
|
189
|
+
// re-request a reload so dynamic configs that need a real agent can finally
|
|
190
|
+
// generate. Skip for static configs — they don't need an agent and the
|
|
191
|
+
// initial mount reload already handled them. Skip when the target agent
|
|
192
|
+
// is already in the registry — the initial reload already covered it, and
|
|
193
|
+
// re-firing on every subsequent `onAgentsChanged` (e.g. dev-mode hot
|
|
194
|
+
// reloads, sibling chat configs mounting) would stack overlapping
|
|
195
|
+
// generations.
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (!normalizedConfig || !isDynamicConfigType) return;
|
|
198
|
+
if (!targetAgentId) return;
|
|
199
|
+
|
|
200
|
+
const initiallyPresent = !!copilotkit.getAgent(targetAgentId);
|
|
201
|
+
if (initiallyPresent) return;
|
|
202
|
+
|
|
203
|
+
const subscription = copilotkit.subscribe({
|
|
204
|
+
onAgentsChanged: () => {
|
|
205
|
+
if (copilotkit.getAgent(targetAgentId)) {
|
|
206
|
+
requestReload();
|
|
207
|
+
subscription.unsubscribe();
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
return () => {
|
|
212
|
+
subscription.unsubscribe();
|
|
213
|
+
};
|
|
214
|
+
}, [
|
|
215
|
+
copilotkit,
|
|
216
|
+
normalizedConfig,
|
|
217
|
+
isDynamicConfigType,
|
|
218
|
+
targetAgentId,
|
|
219
|
+
requestReload,
|
|
220
|
+
]);
|
|
172
221
|
}
|
|
173
222
|
|
|
174
223
|
function isDynamicConfig(
|
|
@@ -253,6 +253,13 @@ export function useThreads({
|
|
|
253
253
|
// realtime subscription or cached thread list stays usable while the
|
|
254
254
|
// runtime recovers, and we don't re-trigger a fetch storm on transitions.
|
|
255
255
|
const runtimeStatus = copilotkit.runtimeConnectionStatus;
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
copilotkit.registerThreadStore(agentId, store);
|
|
258
|
+
return () => {
|
|
259
|
+
copilotkit.unregisterThreadStore(agentId);
|
|
260
|
+
};
|
|
261
|
+
}, [copilotkit, agentId, store]);
|
|
262
|
+
|
|
256
263
|
useEffect(() => {
|
|
257
264
|
if (!copilotkit.runtimeUrl) {
|
|
258
265
|
store.setContext(null);
|
|
@@ -283,7 +290,6 @@ export function useThreads({
|
|
|
283
290
|
headersKey,
|
|
284
291
|
copilotkit.intelligence?.wsUrl,
|
|
285
292
|
agentId,
|
|
286
|
-
copilotkit.headers,
|
|
287
293
|
includeArchived,
|
|
288
294
|
limit,
|
|
289
295
|
]);
|