@copilotkit/react-core 1.56.5-canary.1777972218 → 1.57.0

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 (45) hide show
  1. package/dist/{copilotkit-BVK_b6St.mjs → copilotkit-CPe2-340.mjs} +177 -330
  2. package/dist/copilotkit-CPe2-340.mjs.map +1 -0
  3. package/dist/{copilotkit-DV9LwRgi.d.mts → copilotkit-DFaI4j2r.d.mts} +6 -52
  4. package/dist/copilotkit-DFaI4j2r.d.mts.map +1 -0
  5. package/dist/{copilotkit-BGIsblrk.cjs → copilotkit-DGbvw8n2.cjs} +176 -335
  6. package/dist/copilotkit-DGbvw8n2.cjs.map +1 -0
  7. package/dist/{copilotkit-Bc7kZ72T.d.cts → copilotkit-Dg4r4Gi_.d.cts} +6 -52
  8. package/dist/copilotkit-Dg4r4Gi_.d.cts.map +1 -0
  9. package/dist/index.cjs +5 -2
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +1 -1
  12. package/dist/index.d.mts +1 -1
  13. package/dist/index.mjs +5 -2
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/index.umd.js +172 -117
  16. package/dist/index.umd.js.map +1 -1
  17. package/dist/v2/index.cjs +1 -2
  18. package/dist/v2/index.css +1 -1
  19. package/dist/v2/index.d.cts +2 -2
  20. package/dist/v2/index.d.mts +2 -2
  21. package/dist/v2/index.mjs +2 -2
  22. package/dist/v2/index.umd.js +182 -340
  23. package/dist/v2/index.umd.js.map +1 -1
  24. package/package.json +6 -6
  25. package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +6 -5
  26. package/src/hooks/use-copilot-chat_internal.ts +1 -0
  27. package/src/v2/components/chat/CopilotChat.tsx +1 -2
  28. package/src/v2/components/chat/CopilotChatMessageView.tsx +13 -124
  29. package/src/v2/components/chat/CopilotChatView.tsx +2 -2
  30. package/src/v2/components/chat/__tests__/CopilotChat.welcomeGate.test.tsx +3 -1
  31. package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +25 -29
  32. package/src/v2/components/chat/__tests__/MCPAppsUiMessage.e2e.test.tsx +60 -5
  33. package/src/v2/components/index.ts +0 -1
  34. package/src/v2/hooks/__tests__/use-agent-thread-isolation.test.tsx +333 -0
  35. package/src/v2/hooks/use-agent.tsx +116 -7
  36. package/src/v2/hooks/use-render-activity-message.tsx +11 -3
  37. package/src/v2/hooks/use-render-custom-messages.tsx +6 -1
  38. package/src/v2/styles/globals.css +0 -112
  39. package/dist/copilotkit-BGIsblrk.cjs.map +0 -1
  40. package/dist/copilotkit-BVK_b6St.mjs.map +0 -1
  41. package/dist/copilotkit-Bc7kZ72T.d.cts.map +0 -1
  42. package/dist/copilotkit-DV9LwRgi.d.mts.map +0 -1
  43. package/src/v2/components/intelligence-indicator/IntelligenceIndicator.tsx +0 -265
  44. package/src/v2/components/intelligence-indicator/__tests__/IntelligenceIndicator.e2e.test.tsx +0 -362
  45. package/src/v2/components/intelligence-indicator/index.ts +0 -2
@@ -0,0 +1,333 @@
1
+ import React from "react";
2
+ import { render } from "@testing-library/react";
3
+ import { renderHook } from "@testing-library/react";
4
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
5
+ import {
6
+ AbstractAgent,
7
+ type BaseEvent,
8
+ type RunAgentInput,
9
+ } from "@ag-ui/client";
10
+ import { useCopilotKit } from "../../providers/CopilotKitProvider";
11
+ import { useAgent } from "../use-agent";
12
+ import { CopilotKitCoreRuntimeConnectionStatus } from "@copilotkit/core";
13
+ import { Observable } from "rxjs";
14
+
15
+ vi.mock("../../providers/CopilotKitProvider", () => ({
16
+ useCopilotKit: vi.fn(),
17
+ }));
18
+
19
+ const mockUseCopilotKit = useCopilotKit as ReturnType<typeof vi.fn>;
20
+
21
+ /**
22
+ * A minimal mock agent whose clone() returns a NEW instance and copies
23
+ * messages from the source. This is essential for testing per-thread
24
+ * isolation — each clone must be a distinct object that starts with the
25
+ * source's state so that cloneForThread's setMessages([]) / setState({})
26
+ * calls are meaningful (not vacuously true on an already-empty clone).
27
+ */
28
+ class CloneableAgent extends AbstractAgent {
29
+ clone(): CloneableAgent {
30
+ const cloned = new CloneableAgent();
31
+ cloned.agentId = this.agentId;
32
+ // Copy messages so cloneForThread's setMessages([]) actually clears state
33
+ cloned.setMessages([...this.messages]);
34
+ return cloned;
35
+ }
36
+
37
+ run(_input: RunAgentInput): Observable<BaseEvent> {
38
+ return new Observable();
39
+ }
40
+ }
41
+
42
+ describe("useAgent thread isolation", () => {
43
+ let mockCopilotkit: {
44
+ getAgent: ReturnType<typeof vi.fn>;
45
+ runtimeUrl: string | undefined;
46
+ runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus;
47
+ runtimeTransport: string;
48
+ headers: Record<string, string>;
49
+ agents: Record<string, AbstractAgent>;
50
+ subscribeToAgentWithOptions: (
51
+ agent: AbstractAgent,
52
+ subscriber: any,
53
+ ) => { unsubscribe: () => void };
54
+ };
55
+
56
+ let registeredAgent: CloneableAgent;
57
+
58
+ beforeEach(() => {
59
+ registeredAgent = new CloneableAgent();
60
+ registeredAgent.agentId = "my-agent";
61
+
62
+ mockCopilotkit = {
63
+ getAgent: vi.fn((id: string) =>
64
+ id === "my-agent" ? registeredAgent : undefined,
65
+ ),
66
+ runtimeUrl: "http://localhost:3000/api/copilotkit",
67
+ runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus.Connected,
68
+ runtimeTransport: "rest",
69
+ headers: {},
70
+ agents: { "my-agent": registeredAgent },
71
+ subscribeToAgentWithOptions: (agent, subscriber) =>
72
+ agent.subscribe(subscriber),
73
+ };
74
+
75
+ mockUseCopilotKit.mockReturnValue({
76
+ copilotkit: mockCopilotkit,
77
+ executingToolCallIds: new Set(),
78
+ });
79
+ });
80
+
81
+ afterEach(() => {
82
+ vi.restoreAllMocks();
83
+ });
84
+
85
+ it("returns different agent instances for different threadIds with the same agentId", () => {
86
+ const agents: Record<string, AbstractAgent> = {};
87
+
88
+ function TrackerA() {
89
+ const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-a" });
90
+ agents["a"] = agent;
91
+ return null;
92
+ }
93
+
94
+ function TrackerB() {
95
+ const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-b" });
96
+ agents["b"] = agent;
97
+ return null;
98
+ }
99
+
100
+ render(
101
+ <>
102
+ <TrackerA />
103
+ <TrackerB />
104
+ </>,
105
+ );
106
+
107
+ expect(agents["a"]).toBeDefined();
108
+ expect(agents["b"]).toBeDefined();
109
+ expect(agents["a"]).not.toBe(agents["b"]);
110
+ });
111
+
112
+ it("returns the same cached instance for the same (agentId, threadId) across re-renders", () => {
113
+ const instances: AbstractAgent[] = [];
114
+
115
+ function Tracker() {
116
+ const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-x" });
117
+ instances.push(agent);
118
+ return null;
119
+ }
120
+
121
+ const { rerender } = render(<Tracker />);
122
+ rerender(<Tracker />);
123
+
124
+ expect(instances.length).toBe(2);
125
+ expect(instances[0]).toBe(instances[1]);
126
+ });
127
+
128
+ it("returns the shared registry agent when no threadId is provided (backward compat)", () => {
129
+ let captured: AbstractAgent | undefined;
130
+
131
+ function Tracker() {
132
+ const { agent } = useAgent({ agentId: "my-agent" });
133
+ captured = agent;
134
+ return null;
135
+ }
136
+
137
+ render(<Tracker />);
138
+ expect(captured).toBe(registeredAgent);
139
+ });
140
+
141
+ it("isolates messages between thread-specific agents", () => {
142
+ // Pre-populate the source agent so CloneableAgent.clone() copies the
143
+ // message into each clone — this makes cloneForThread's setMessages([])
144
+ // meaningful rather than vacuously true on an already-empty clone.
145
+ registeredAgent.addMessage({
146
+ id: "source-msg",
147
+ role: "user",
148
+ content: "pre-existing on source",
149
+ });
150
+
151
+ const agents: Record<string, AbstractAgent> = {};
152
+
153
+ function TrackerA() {
154
+ const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-a" });
155
+ agents["a"] = agent;
156
+ return null;
157
+ }
158
+
159
+ function TrackerB() {
160
+ const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-b" });
161
+ agents["b"] = agent;
162
+ return null;
163
+ }
164
+
165
+ render(
166
+ <>
167
+ <TrackerA />
168
+ <TrackerB />
169
+ </>,
170
+ );
171
+
172
+ // Both clones should start empty even though the source had a message —
173
+ // cloneForThread must have called setMessages([]) on each clone.
174
+ expect(agents["a"]!.messages).toHaveLength(0);
175
+ expect(agents["b"]!.messages).toHaveLength(0);
176
+
177
+ // Adding a message to thread A must not affect thread B
178
+ agents["a"]!.addMessage({
179
+ id: "msg-1",
180
+ role: "user",
181
+ content: "hello from thread A",
182
+ });
183
+
184
+ expect(agents["a"]!.messages).toHaveLength(1);
185
+ expect(agents["b"]!.messages).toHaveLength(0);
186
+ });
187
+
188
+ it("sets threadId on cloned agents", () => {
189
+ const agents: Record<string, AbstractAgent> = {};
190
+
191
+ function TrackerA() {
192
+ const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-a" });
193
+ agents["a"] = agent;
194
+ return null;
195
+ }
196
+
197
+ function TrackerB() {
198
+ const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-b" });
199
+ agents["b"] = agent;
200
+ return null;
201
+ }
202
+
203
+ render(
204
+ <>
205
+ <TrackerA />
206
+ <TrackerB />
207
+ </>,
208
+ );
209
+
210
+ expect(agents["a"]!.threadId).toBe("thread-a");
211
+ expect(agents["b"]!.threadId).toBe("thread-b");
212
+ });
213
+
214
+ it("invalidates stale clone when the registry agent is replaced", () => {
215
+ // Simulates reconnect / hot-reload: copilotkit.agents holds a new object.
216
+ const { result, rerender } = renderHook(
217
+ ({ tid }: { tid: string }) =>
218
+ useAgent({ agentId: "my-agent", threadId: tid }),
219
+ { initialProps: { tid: "thread-a" } },
220
+ );
221
+
222
+ const firstClone = result.current.agent;
223
+ expect(firstClone).not.toBe(registeredAgent); // it's a clone
224
+
225
+ // Replace the registry agent
226
+ const replacementAgent = new CloneableAgent();
227
+ replacementAgent.agentId = "my-agent";
228
+
229
+ mockCopilotkit.agents = { "my-agent": replacementAgent };
230
+ mockCopilotkit.getAgent.mockImplementation((id: string) =>
231
+ id === "my-agent" ? replacementAgent : undefined,
232
+ );
233
+ mockUseCopilotKit.mockReturnValue({
234
+ copilotkit: { ...mockCopilotkit },
235
+ executingToolCallIds: new Set(),
236
+ });
237
+
238
+ rerender({ tid: "thread-a" });
239
+
240
+ const secondClone = result.current.agent;
241
+ expect(secondClone).not.toBe(firstClone); // stale clone was invalidated
242
+ expect(secondClone).not.toBe(replacementAgent); // still a clone, not the source
243
+ });
244
+
245
+ it("switching threadId returns a fresh clone; switching back returns the cached one", () => {
246
+ const { result, rerender } = renderHook(
247
+ ({ tid }: { tid: string }) =>
248
+ useAgent({ agentId: "my-agent", threadId: tid }),
249
+ { initialProps: { tid: "thread-a" } },
250
+ );
251
+
252
+ const cloneA = result.current.agent;
253
+
254
+ rerender({ tid: "thread-b" });
255
+ const cloneB = result.current.agent;
256
+ expect(cloneB).not.toBe(cloneA);
257
+
258
+ // Switching back to thread-a should return the originally cached clone
259
+ rerender({ tid: "thread-a" });
260
+ expect(result.current.agent).toBe(cloneA);
261
+ });
262
+
263
+ it("uses a fresh clone with correct threadId when provisional transitions to real agent", () => {
264
+ // Start in Disconnected state — a provisional is created
265
+ mockCopilotkit.runtimeConnectionStatus =
266
+ CopilotKitCoreRuntimeConnectionStatus.Disconnected;
267
+ mockCopilotkit.getAgent.mockReturnValue(undefined);
268
+ mockCopilotkit.agents = {};
269
+ mockUseCopilotKit.mockReturnValue({
270
+ copilotkit: { ...mockCopilotkit },
271
+ executingToolCallIds: new Set(),
272
+ });
273
+
274
+ const { result, rerender } = renderHook(() =>
275
+ useAgent({ agentId: "my-agent", threadId: "thread-a" }),
276
+ );
277
+
278
+ const provisional = result.current.agent;
279
+ expect(provisional.threadId).toBe("thread-a");
280
+
281
+ // Real agent appears (runtime connected and agent registered)
282
+ mockCopilotkit.runtimeConnectionStatus =
283
+ CopilotKitCoreRuntimeConnectionStatus.Connected;
284
+ mockCopilotkit.getAgent.mockImplementation((id: string) =>
285
+ id === "my-agent" ? registeredAgent : undefined,
286
+ );
287
+ mockCopilotkit.agents = { "my-agent": registeredAgent };
288
+ mockUseCopilotKit.mockReturnValue({
289
+ copilotkit: { ...mockCopilotkit },
290
+ executingToolCallIds: new Set(),
291
+ });
292
+
293
+ rerender();
294
+
295
+ const realClone = result.current.agent;
296
+ expect(realClone).not.toBe(provisional); // provisional replaced by real clone
297
+ expect(realClone).not.toBe(registeredAgent); // it's a clone, not the source
298
+ expect(realClone.threadId).toBe("thread-a");
299
+ });
300
+
301
+ it("uses composite key for provisional agents when threadId is provided", () => {
302
+ // Put runtime in Disconnected state so provisionals are created
303
+ mockCopilotkit.runtimeConnectionStatus =
304
+ CopilotKitCoreRuntimeConnectionStatus.Disconnected;
305
+ mockCopilotkit.getAgent.mockReturnValue(undefined);
306
+ mockCopilotkit.agents = {};
307
+
308
+ const agents: Record<string, AbstractAgent> = {};
309
+
310
+ function TrackerA() {
311
+ const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-a" });
312
+ agents["a"] = agent;
313
+ return null;
314
+ }
315
+
316
+ function TrackerB() {
317
+ const { agent } = useAgent({ agentId: "my-agent", threadId: "thread-b" });
318
+ agents["b"] = agent;
319
+ return null;
320
+ }
321
+
322
+ render(
323
+ <>
324
+ <TrackerA />
325
+ <TrackerB />
326
+ </>,
327
+ );
328
+
329
+ expect(agents["a"]).not.toBe(agents["b"]);
330
+ expect(agents["a"]!.threadId).toBe("thread-a");
331
+ expect(agents["b"]!.threadId).toBe("thread-b");
332
+ });
333
+ });
@@ -1,4 +1,5 @@
1
1
  import { useCopilotKit } from "../providers/CopilotKitProvider";
2
+ import { useCopilotChatConfiguration } from "../providers/CopilotChatConfigurationProvider";
2
3
  import { useMemo, useEffect, useReducer, useRef } from "react";
3
4
  import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
4
5
  import { AbstractAgent, HttpAgent } from "@ag-ui/client";
@@ -22,6 +23,7 @@ const ALL_UPDATES: UseAgentUpdate[] = [
22
23
 
23
24
  export interface UseAgentProps {
24
25
  agentId?: string;
26
+ threadId?: string;
25
27
  updates?: UseAgentUpdate[];
26
28
  /**
27
29
  * Throttle interval (in milliseconds) for re-renders triggered by
@@ -48,7 +50,80 @@ export interface UseAgentProps {
48
50
  throttleMs?: number;
49
51
  }
50
52
 
51
- export function useAgent({ agentId, updates, throttleMs }: UseAgentProps = {}) {
53
+ /**
54
+ * Clone a registry agent for per-thread isolation.
55
+ * Copies agent configuration (transport, headers, etc.) but resets conversation
56
+ * state (messages, threadId, state) so each thread starts fresh.
57
+ */
58
+ function cloneForThread(
59
+ source: AbstractAgent,
60
+ threadId: string,
61
+ headers: Record<string, string>,
62
+ ): AbstractAgent {
63
+ const clone = source.clone();
64
+ if (clone === source) {
65
+ throw new Error(
66
+ `useAgent: ${source.constructor.name}.clone() returned the same instance. ` +
67
+ `clone() must return a new, independent object.`,
68
+ );
69
+ }
70
+ clone.threadId = threadId;
71
+ clone.setMessages([]);
72
+ clone.setState({});
73
+ if (clone instanceof HttpAgent) {
74
+ clone.headers = { ...headers };
75
+ }
76
+ return clone;
77
+ }
78
+
79
+ /**
80
+ * Module-level WeakMap: registryAgent → (threadId → clone).
81
+ * Shared across all useAgent() calls so that every component using the same
82
+ * (agentId, threadId) pair receives the same agent instance. Using WeakMap
83
+ * ensures the clone map is garbage-collected when the registry agent is
84
+ * replaced (e.g. after reconnect or hot-reload).
85
+ */
86
+ export const globalThreadCloneMap = new WeakMap<
87
+ AbstractAgent,
88
+ Map<string, AbstractAgent>
89
+ >();
90
+
91
+ /**
92
+ * Look up an existing per-thread clone without creating one.
93
+ * Returns undefined when no clone has been created yet for this pair.
94
+ */
95
+ export function getThreadClone(
96
+ registryAgent: AbstractAgent | undefined | null,
97
+ threadId: string | undefined | null,
98
+ ): AbstractAgent | undefined {
99
+ if (!registryAgent || !threadId) return undefined;
100
+ return globalThreadCloneMap.get(registryAgent)?.get(threadId);
101
+ }
102
+
103
+ function getOrCreateThreadClone(
104
+ existing: AbstractAgent,
105
+ threadId: string,
106
+ headers: Record<string, string>,
107
+ ): AbstractAgent {
108
+ let byThread = globalThreadCloneMap.get(existing);
109
+ if (!byThread) {
110
+ byThread = new Map();
111
+ globalThreadCloneMap.set(existing, byThread);
112
+ }
113
+ const cached = byThread.get(threadId);
114
+ if (cached) return cached;
115
+
116
+ const clone = cloneForThread(existing, threadId, headers);
117
+ byThread.set(threadId, clone);
118
+ return clone;
119
+ }
120
+
121
+ export function useAgent({
122
+ agentId,
123
+ threadId,
124
+ updates,
125
+ throttleMs,
126
+ }: UseAgentProps = {}) {
52
127
  agentId ??= DEFAULT_AGENT_ID;
53
128
 
54
129
  const { copilotkit } = useCopilotKit();
@@ -56,6 +131,12 @@ export function useAgent({ agentId, updates, throttleMs }: UseAgentProps = {}) {
56
131
  // subscribeToAgentWithOptions reads it from the core instance, but React needs the dep
57
132
  // to know when to re-subscribe.
58
133
  const providerThrottleMs = copilotkit.defaultThrottleMs;
134
+ // Fall back to the enclosing CopilotChatConfigurationProvider's threadId so
135
+ // that useAgent() called without explicit threadId (e.g. inside a custom
136
+ // message renderer) automatically uses the same per-thread clone as the
137
+ // CopilotChat component it lives within.
138
+ const chatConfig = useCopilotChatConfiguration();
139
+ threadId ??= chatConfig?.threadId;
59
140
 
60
141
  const [, forceUpdate] = useReducer((x) => x + 1, 0);
61
142
 
@@ -72,11 +153,29 @@ export function useAgent({ agentId, updates, throttleMs }: UseAgentProps = {}) {
72
153
  );
73
154
 
74
155
  const agent: AbstractAgent = useMemo(() => {
156
+ // Use a composite key when threadId is provided so that different threads
157
+ // for the same agent get independent instances.
158
+ const cacheKey = threadId ? `${agentId}:${threadId}` : agentId;
159
+
75
160
  const existing = copilotkit.getAgent(agentId);
76
161
  if (existing) {
77
- // Real agent found — clear any cached provisional for this ID
162
+ // Real agent found — clear any cached provisionals for this key and the
163
+ // bare agentId key (handles the case where a provisional was created
164
+ // before threadId was available, then the component re-renders with one).
165
+ provisionalAgentCache.current.delete(cacheKey);
78
166
  provisionalAgentCache.current.delete(agentId);
79
- return existing;
167
+
168
+ if (!threadId) {
169
+ // No threadId — return the shared registry agent (original behavior)
170
+ return existing;
171
+ }
172
+
173
+ // threadId provided — return the shared per-thread clone.
174
+ // The global WeakMap ensures all components using the same
175
+ // (registryAgent, threadId) pair receive the same instance, so state
176
+ // mutations (addMessage, setState) are visible everywhere. The WeakMap
177
+ // entry is GC-collected automatically when the registry agent is replaced.
178
+ return getOrCreateThreadClone(existing, threadId, copilotkit.headers);
80
179
  }
81
180
 
82
181
  const isRuntimeConfigured = copilotkit.runtimeUrl !== undefined;
@@ -89,7 +188,7 @@ export function useAgent({ agentId, updates, throttleMs }: UseAgentProps = {}) {
89
188
  status === CopilotKitCoreRuntimeConnectionStatus.Connecting)
90
189
  ) {
91
190
  // Return cached provisional if available (keeps reference stable)
92
- const cached = provisionalAgentCache.current.get(agentId);
191
+ const cached = provisionalAgentCache.current.get(cacheKey);
93
192
  if (cached) {
94
193
  // Update headers on the cached agent in case they changed
95
194
  cached.headers = { ...copilotkit.headers };
@@ -104,7 +203,10 @@ export function useAgent({ agentId, updates, throttleMs }: UseAgentProps = {}) {
104
203
  });
105
204
  // Apply current headers so runs/connects inherit them
106
205
  provisional.headers = { ...copilotkit.headers };
107
- provisionalAgentCache.current.set(agentId, provisional);
206
+ if (threadId) {
207
+ provisional.threadId = threadId;
208
+ }
209
+ provisionalAgentCache.current.set(cacheKey, provisional);
108
210
  return provisional;
109
211
  }
110
212
 
@@ -117,7 +219,10 @@ export function useAgent({ agentId, updates, throttleMs }: UseAgentProps = {}) {
117
219
  isRuntimeConfigured &&
118
220
  status === CopilotKitCoreRuntimeConnectionStatus.Error
119
221
  ) {
120
- const cached = provisionalAgentCache.current.get(agentId);
222
+ // Cache the provisional so that dep changes while in Error state (e.g.
223
+ // headers update) return the same agent reference, matching the
224
+ // Disconnected/Connecting path and preventing spurious re-subscriptions.
225
+ const cached = provisionalAgentCache.current.get(cacheKey);
121
226
  if (cached) {
122
227
  cached.headers = { ...copilotkit.headers };
123
228
  return cached;
@@ -129,7 +234,10 @@ export function useAgent({ agentId, updates, throttleMs }: UseAgentProps = {}) {
129
234
  runtimeMode: "pending",
130
235
  });
131
236
  provisional.headers = { ...copilotkit.headers };
132
- provisionalAgentCache.current.set(agentId, provisional);
237
+ if (threadId) {
238
+ provisional.threadId = threadId;
239
+ }
240
+ provisionalAgentCache.current.set(cacheKey, provisional);
133
241
  return provisional;
134
242
  }
135
243
 
@@ -148,6 +256,7 @@ export function useAgent({ agentId, updates, throttleMs }: UseAgentProps = {}) {
148
256
  // eslint-disable-next-line react-hooks/exhaustive-deps
149
257
  }, [
150
258
  agentId,
259
+ threadId,
151
260
  copilotkit.agents,
152
261
  copilotkit.runtimeConnectionStatus,
153
262
  copilotkit.runtimeUrl,
@@ -3,10 +3,12 @@ import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
3
3
  import { useCopilotKit, useCopilotChatConfiguration } from "../providers";
4
4
  import { useCallback, useMemo } from "react";
5
5
  import { ReactActivityMessageRenderer } from "../types";
6
+ import { getThreadClone } from "./use-agent";
6
7
 
7
8
  export function useRenderActivityMessage() {
8
9
  const { copilotkit } = useCopilotKit();
9
- const agentId = useCopilotChatConfiguration()?.agentId ?? DEFAULT_AGENT_ID;
10
+ const config = useCopilotChatConfiguration();
11
+ const agentId = config?.agentId ?? DEFAULT_AGENT_ID;
10
12
 
11
13
  const renderers = copilotkit.renderActivityMessages;
12
14
 
@@ -50,7 +52,13 @@ export function useRenderActivityMessage() {
50
52
  }
51
53
 
52
54
  const Component = renderer.render;
53
- const agent = copilotkit.getAgent(agentId);
55
+ // Prefer the per-thread clone so that handleAction in ReactSurfaceHost
56
+ // calls runAgent on the same agent instance that CopilotChat renders from.
57
+ // Without this, button clicks accumulate messages on the registry agent
58
+ // while CopilotChat displays from the clone — responses appear to vanish.
59
+ const registryAgent = copilotkit.getAgent(agentId);
60
+ const agent =
61
+ getThreadClone(registryAgent, config?.threadId) ?? registryAgent;
54
62
 
55
63
  return (
56
64
  <Component
@@ -62,7 +70,7 @@ export function useRenderActivityMessage() {
62
70
  />
63
71
  );
64
72
  },
65
- [agentId, copilotkit, findRenderer],
73
+ [agentId, config?.threadId, copilotkit, findRenderer],
66
74
  );
67
75
 
68
76
  return useMemo(
@@ -1,4 +1,5 @@
1
1
  import { useCopilotChatConfiguration, useCopilotKit } from "../providers";
2
+ import { getThreadClone } from "./use-agent";
2
3
  import { ReactCustomMessageRendererPosition } from "../types/react-custom-message-renderer";
3
4
  import { Message } from "@ag-ui/core";
4
5
 
@@ -38,7 +39,11 @@ export function useRenderCustomMessages() {
38
39
  copilotkit.getRunIdForMessage(agentId, threadId, message.id) ??
39
40
  copilotkit.getRunIdsForThread(agentId, threadId).slice(-1)[0];
40
41
  const runId = resolvedRunId ?? `missing-run-id:${message.id}`;
41
- const agent = copilotkit.getAgent(agentId);
42
+ // Prefer the per-thread clone so that agent.messages reflects the actual
43
+ // conversation state (messages live on the clone, not the registry agent).
44
+ // Fall back to the registry agent when no clone exists (no threadId).
45
+ const registryAgent = copilotkit.getAgent(agentId);
46
+ const agent = getThreadClone(registryAgent, threadId) ?? registryAgent;
42
47
  if (!agent) {
43
48
  return null;
44
49
  }
@@ -229,115 +229,3 @@
229
229
  background-repeat: no-repeat;
230
230
  background-size: 100% 100%;
231
231
  }
232
- /*
233
- * IntelligenceIndicator pill — visual styles ported from
234
- * CopilotKit/Intelligence #155.
235
- *
236
- * Violet/indigo glassmorphism — text #5B21B6, icon #7C3AED, border
237
- * #9599E0, gradient stop #EEE6FE, soft shadow #5E64AD.
238
- */
239
- .cpk-intelligence-pill {
240
- display: inline-flex;
241
- align-items: center;
242
- gap: 0.55rem;
243
- margin: 0.4rem 0;
244
- padding: 0.4rem 0.85rem;
245
- border: 1px solid rgb(149 153 224 / 0.32);
246
- border-radius: 999px;
247
- background: linear-gradient(
248
- 135deg,
249
- rgb(255 255 255 / 0.55) 0%,
250
- rgb(238 230 254 / 0.55) 100%
251
- );
252
- -webkit-backdrop-filter: blur(14px) saturate(160%);
253
- backdrop-filter: blur(14px) saturate(160%);
254
- color: rgb(91 33 182 / 0.92);
255
- font-size: 0.78rem;
256
- font-weight: 500;
257
- letter-spacing: 0.01em;
258
- white-space: nowrap;
259
- box-shadow:
260
- 0 1px 2px rgb(94 100 173 / 0.06),
261
- 0 8px 24px rgb(149 153 224 / 0.08),
262
- inset 0 1px 0 rgb(255 255 255 / 0.6);
263
- opacity: 0;
264
- transform: translateY(2px);
265
- animation: cpk-intelligence-pill-fade-in 280ms ease-out forwards;
266
- }
267
-
268
- .cpk-intelligence-pill--fading {
269
- /* Switching the `animation` property cancels the in-flight fade-in
270
- and replaces the held final-value (opacity:1 from `forwards`) with
271
- this fade-out keyframe set. Plain `opacity: 0` declarations would
272
- otherwise lose to the animation's held value. */
273
- animation: cpk-intelligence-pill-fade-out 480ms ease-out forwards;
274
- pointer-events: none;
275
- }
276
-
277
- .cpk-intelligence-pill__icon {
278
- display: block;
279
- flex-shrink: 0;
280
- color: rgb(124 58 237);
281
- }
282
-
283
- .cpk-intelligence-pill__ring {
284
- stroke: currentColor;
285
- stroke-dasharray: 40 16.55;
286
- stroke-dashoffset: 0;
287
- transform-origin: 12px 12px;
288
- animation: cpk-intelligence-pill-spin 0.9s linear infinite;
289
- transition: stroke-dasharray 320ms ease-out;
290
- }
291
-
292
- .cpk-intelligence-pill__ring--done {
293
- stroke-dasharray: 56.55 0;
294
- animation: none;
295
- }
296
-
297
- .cpk-intelligence-pill__check {
298
- stroke: currentColor;
299
- stroke-dasharray: 12;
300
- stroke-dashoffset: 12;
301
- /* Opacity zero hides the rounded `stroke-linecap` end-cap that would
302
- otherwise leak through as a stationary dot at path end (16, 9.5)
303
- during spinner phase. The `dasharray + dashoffset` zeroes the
304
- visible stroke length, but the cap is drawn at the dash boundary
305
- regardless, so we need opacity to fully hide it. */
306
- opacity: 0;
307
- transition:
308
- stroke-dashoffset 320ms ease-out 200ms,
309
- opacity 200ms ease-out 200ms;
310
- }
311
-
312
- .cpk-intelligence-pill__check--shown {
313
- stroke-dashoffset: 0;
314
- opacity: 1;
315
- }
316
-
317
- @keyframes cpk-intelligence-pill-fade-in {
318
- from {
319
- opacity: 0;
320
- transform: translateY(2px);
321
- }
322
- to {
323
- opacity: 1;
324
- transform: translateY(0);
325
- }
326
- }
327
-
328
- @keyframes cpk-intelligence-pill-fade-out {
329
- from {
330
- opacity: 1;
331
- transform: translateY(0);
332
- }
333
- to {
334
- opacity: 0;
335
- transform: translateY(-2px);
336
- }
337
- }
338
-
339
- @keyframes cpk-intelligence-pill-spin {
340
- to {
341
- transform: rotate(360deg);
342
- }
343
- }