@copilotkit/react-core 1.57.0-canary.1778082736 → 1.57.0-canary.1778116520

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 (84) hide show
  1. package/dist/{copilotkit-B_k0HSNz.cjs → copilotkit-BLlkMAjx.cjs} +368 -502
  2. package/dist/copilotkit-BLlkMAjx.cjs.map +1 -0
  3. package/dist/{copilotkit-BNlJq5UO.d.mts → copilotkit-BYnbIBN5.d.mts} +10 -62
  4. package/dist/copilotkit-BYnbIBN5.d.mts.map +1 -0
  5. package/dist/{copilotkit-ak8sGvQr.mjs → copilotkit-N0YiBG5S.mjs} +369 -497
  6. package/dist/copilotkit-N0YiBG5S.mjs.map +1 -0
  7. package/dist/{copilotkit-DgC5oCFO.d.cts → copilotkit-vx_R9p-O.d.cts} +10 -62
  8. package/dist/copilotkit-vx_R9p-O.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 +409 -347
  16. package/dist/index.umd.js.map +1 -1
  17. package/dist/v2/context.cjs +135 -0
  18. package/dist/v2/context.cjs.map +1 -0
  19. package/dist/v2/context.d.cts +148 -0
  20. package/dist/v2/context.d.cts.map +1 -0
  21. package/dist/v2/context.d.mts +148 -0
  22. package/dist/v2/context.d.mts.map +1 -0
  23. package/dist/v2/context.mjs +129 -0
  24. package/dist/v2/context.mjs.map +1 -0
  25. package/dist/v2/headless.cjs +1085 -0
  26. package/dist/v2/headless.cjs.map +1 -0
  27. package/dist/v2/headless.d.cts +607 -0
  28. package/dist/v2/headless.d.cts.map +1 -0
  29. package/dist/v2/headless.d.mts +514 -0
  30. package/dist/v2/headless.d.mts.map +1 -0
  31. package/dist/v2/headless.mjs +1039 -0
  32. package/dist/v2/headless.mjs.map +1 -0
  33. package/dist/v2/index.cjs +1 -2
  34. package/dist/v2/index.css +1 -1
  35. package/dist/v2/index.d.cts +2 -2
  36. package/dist/v2/index.d.mts +2 -2
  37. package/dist/v2/index.mjs +2 -2
  38. package/dist/v2/index.umd.js +387 -523
  39. package/dist/v2/index.umd.js.map +1 -1
  40. package/package.json +14 -6
  41. package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +6 -5
  42. package/src/hooks/use-copilot-chat_internal.ts +1 -0
  43. package/src/v2/components/MCPAppsActivityRenderer.tsx +9 -3
  44. package/src/v2/components/chat/CopilotChat.tsx +1 -2
  45. package/src/v2/components/chat/CopilotChatMessageView.tsx +9 -24
  46. package/src/v2/components/chat/CopilotChatView.tsx +2 -2
  47. package/src/v2/components/chat/__tests__/CopilotChat.welcomeGate.test.tsx +3 -1
  48. package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +25 -29
  49. package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +102 -0
  50. package/src/v2/components/chat/__tests__/MCPAppsUiMessage.e2e.test.tsx +60 -5
  51. package/src/v2/components/index.ts +0 -1
  52. package/src/v2/context.ts +62 -0
  53. package/src/v2/headless.ts +42 -0
  54. package/src/v2/hooks/__tests__/standard-schema.test.tsx +2 -2
  55. package/src/v2/hooks/__tests__/use-agent-context.test.tsx +3 -3
  56. package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +3 -3
  57. package/src/v2/hooks/__tests__/use-agent-thread-isolation.test.tsx +333 -0
  58. package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +85 -85
  59. package/src/v2/hooks/__tests__/use-interrupt.test.tsx +2 -2
  60. package/src/v2/hooks/__tests__/use-render-tool.test.tsx +2 -2
  61. package/src/v2/hooks/__tests__/use-threads.test.tsx +2 -2
  62. package/src/v2/hooks/__tests__/zod-regression.test.tsx +2 -2
  63. package/src/v2/hooks/use-agent-context.tsx +1 -1
  64. package/src/v2/hooks/use-agent.tsx +118 -9
  65. package/src/v2/hooks/use-configure-suggestions.tsx +1 -1
  66. package/src/v2/hooks/use-frontend-tool.tsx +2 -2
  67. package/src/v2/hooks/use-human-in-the-loop.tsx +1 -1
  68. package/src/v2/hooks/use-interrupt.tsx +1 -1
  69. package/src/v2/hooks/use-render-activity-message.tsx +11 -3
  70. package/src/v2/hooks/use-render-custom-messages.tsx +6 -1
  71. package/src/v2/hooks/use-render-tool-call.tsx +1 -1
  72. package/src/v2/hooks/use-render-tool.tsx +2 -2
  73. package/src/v2/hooks/use-suggestions.tsx +1 -1
  74. package/src/v2/hooks/use-threads.tsx +1 -1
  75. package/src/v2/providers/CopilotKitProvider.tsx +19 -59
  76. package/src/v2/styles/globals.css +0 -118
  77. package/tsdown.config.ts +75 -0
  78. package/dist/copilotkit-BNlJq5UO.d.mts.map +0 -1
  79. package/dist/copilotkit-B_k0HSNz.cjs.map +0 -1
  80. package/dist/copilotkit-DgC5oCFO.d.cts.map +0 -1
  81. package/dist/copilotkit-ak8sGvQr.mjs.map +0 -1
  82. package/src/v2/components/intelligence-indicator/IntelligenceIndicator.tsx +0 -286
  83. package/src/v2/components/intelligence-indicator/__tests__/IntelligenceIndicator.e2e.test.tsx +0 -464
  84. package/src/v2/components/intelligence-indicator/index.ts +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copilotkit/react-core",
3
- "version": "1.57.0-canary.1778082736",
3
+ "version": "1.57.0-canary.1778116520",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "ai",
@@ -46,6 +46,14 @@
46
46
  "require": "./dist/v2/index.cjs"
47
47
  },
48
48
  "./package.json": "./package.json",
49
+ "./v2/context": {
50
+ "import": "./dist/v2/context.mjs",
51
+ "require": "./dist/v2/context.cjs"
52
+ },
53
+ "./v2/headless": {
54
+ "import": "./dist/v2/headless.mjs",
55
+ "require": "./dist/v2/headless.cjs"
56
+ },
49
57
  "./v2/styles.css": "./dist/v2/index.css"
50
58
  },
51
59
  "publishConfig": {
@@ -73,11 +81,11 @@
73
81
  "untruncate-json": "^0.0.1",
74
82
  "use-stick-to-bottom": "^1.1.1",
75
83
  "zod-to-json-schema": "^3.24.5",
76
- "@copilotkit/a2ui-renderer": "1.57.0-canary.1778082736",
77
- "@copilotkit/core": "1.57.0-canary.1778082736",
78
- "@copilotkit/runtime-client-gql": "1.57.0-canary.1778082736",
79
- "@copilotkit/web-inspector": "1.57.0-canary.1778082736",
80
- "@copilotkit/shared": "1.57.0-canary.1778082736"
84
+ "@copilotkit/a2ui-renderer": "1.57.0-canary.1778116520",
85
+ "@copilotkit/runtime-client-gql": "1.57.0-canary.1778116520",
86
+ "@copilotkit/core": "1.57.0-canary.1778116520",
87
+ "@copilotkit/shared": "1.57.0-canary.1778116520",
88
+ "@copilotkit/web-inspector": "1.57.0-canary.1778116520"
81
89
  },
82
90
  "devDependencies": {
83
91
  "@tailwindcss/cli": "^4.1.11",
@@ -179,9 +179,10 @@ describe("useCopilotChatInternal – connectAgent guard", () => {
179
179
  });
180
180
 
181
181
  it("does not call connectAgent when threadId matches (same agent instance, no re-render)", async () => {
182
- // The wrapper guards via lastConnectedAgentRef: connect fires once per
183
- // agent instance, not once per render. After the first connect, further
184
- // re-renders with the same agent do not trigger another connect.
182
+ // useAgent now returns a per-thread clone, so the wrapper guards via
183
+ // lastConnectedAgentRef: connect fires once per agent instance, not once
184
+ // per render. After the first connect, further re-renders with the same
185
+ // agent do not trigger another connect.
185
186
  mockRuntimeConnectionStatus =
186
187
  CopilotKitCoreRuntimeConnectionStatus.Connected;
187
188
  mockAgent.threadId = "config-thread-id";
@@ -229,13 +230,13 @@ describe("useCopilotChatInternal – connectAgent guard", () => {
229
230
  });
230
231
  });
231
232
 
232
- it("passes resolved agentId to useAgent", () => {
233
+ it("passes config threadId to useAgent", () => {
233
234
  applyMocks();
234
235
 
235
236
  renderHook(() => useCopilotChatInternal(), { wrapper: createWrapper() });
236
237
 
237
238
  expect(vi.mocked(useAgent)).toHaveBeenCalledWith(
238
- expect.objectContaining({ agentId: "test-agent" }),
239
+ expect.objectContaining({ threadId: "config-thread-id" }),
239
240
  );
240
241
  });
241
242
  });
@@ -339,6 +339,7 @@ export function useCopilotChatInternal({
339
339
  const resolvedAgentId = existingConfig?.agentId ?? "default";
340
340
  const { agent } = useAgent({
341
341
  agentId: resolvedAgentId,
342
+ threadId: existingConfig?.threadId,
342
343
  });
343
344
 
344
345
  // Track the last agent instance we called connect() on. Without this,
@@ -281,6 +281,14 @@ export const MCPAppsActivityRenderer: React.FC<MCPAppsActivityRendererProps> =
281
281
  resourceUri: string | null;
282
282
  }>({ inProgress: false, promise: null, resourceUri: null });
283
283
 
284
+ // Destructure resource identifiers so the fetch effect's deps array tracks
285
+ // primitives, not the `content` object reference. `content` gets a new
286
+ // identity on every ACTIVITY_SNAPSHOT from the MCP middleware, which would
287
+ // otherwise re-fire the effect mid-stream and race with `agent.messages`
288
+ // catching up to the server — triggering LangGraph's regenerate path and
289
+ // an infinite "Message not found" loop.
290
+ const { resourceUri, serverHash, serverId } = content;
291
+
284
292
  // Callback to send a message to the iframe
285
293
  const sendToIframe = useCallback((msg: JSONRPCMessage) => {
286
294
  if (iframeRef.current?.contentWindow) {
@@ -328,8 +336,6 @@ export const MCPAppsActivityRenderer: React.FC<MCPAppsActivityRendererProps> =
328
336
  // Effect 0: Fetch the resource content on mount
329
337
  // Uses ref-based deduplication to handle React StrictMode double-mounting
330
338
  useEffect(() => {
331
- const { resourceUri, serverHash, serverId } = content;
332
-
333
339
  // Check if we already have a fetch in progress for this resource
334
340
  // This handles StrictMode double-mounting - second mount reuses first mount's promise
335
341
  if (
@@ -416,7 +422,7 @@ export const MCPAppsActivityRenderer: React.FC<MCPAppsActivityRendererProps> =
416
422
  });
417
423
 
418
424
  // No cleanup needed - we want the fetch to complete even if StrictMode unmounts
419
- }, [agent, content]);
425
+ }, [agent, resourceUri, serverHash, serverId]);
420
426
 
421
427
  // Effect 1: Setup sandbox proxy iframe and communication (after resource is fetched)
422
428
  useEffect(() => {
@@ -118,6 +118,7 @@ export function CopilotChat({
118
118
 
119
119
  const { agent } = useAgent({
120
120
  agentId: resolvedAgentId,
121
+ threadId: resolvedThreadId,
121
122
  throttleMs,
122
123
  });
123
124
  const { copilotkit } = useCopilotKit();
@@ -234,8 +235,6 @@ export function CopilotChat({
234
235
  agent.abortController = connectAbortController;
235
236
  }
236
237
 
237
- agent.threadId = resolvedThreadId;
238
-
239
238
  const connect = async (agent: AbstractAgent) => {
240
239
  try {
241
240
  await copilotkit.connectAgent({ agent });
@@ -8,12 +8,11 @@ import React, {
8
8
  } from "react";
9
9
  import { useVirtualizer } from "@tanstack/react-virtual";
10
10
  import { ScrollElementContext } from "./scroll-element-context";
11
- import type { WithSlots } from "../../lib/slots";
12
- import { renderSlot, isReactComponentType } from "../../lib/slots";
11
+ import { WithSlots, renderSlot, isReactComponentType } from "../../lib/slots";
13
12
  import CopilotChatAssistantMessage from "./CopilotChatAssistantMessage";
14
13
  import CopilotChatUserMessage from "./CopilotChatUserMessage";
15
14
  import CopilotChatReasoningMessage from "./CopilotChatReasoningMessage";
16
- import type {
15
+ import {
17
16
  ActivityMessage,
18
17
  AssistantMessage,
19
18
  Message,
@@ -23,10 +22,9 @@ import type {
23
22
  } from "@ag-ui/core";
24
23
  import { twMerge } from "tailwind-merge";
25
24
  import { useRenderActivityMessage, useRenderCustomMessages } from "../../hooks";
25
+ import { getThreadClone } from "../../hooks/use-agent";
26
26
  import { useCopilotKit } from "../../providers/CopilotKitProvider";
27
27
  import { useCopilotChatConfiguration } from "../../providers/CopilotChatConfigurationProvider";
28
- import { IntelligenceIndicator } from "../intelligence-indicator";
29
- import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
30
28
 
31
29
  /**
32
30
  * Resolves a slot value into a { Component, slotProps } pair, handling the three
@@ -394,14 +392,18 @@ export function CopilotChatMessageView({
394
392
  // Subscribe to state changes so custom message renderers re-render when state updates.
395
393
  useEffect(() => {
396
394
  if (!config?.agentId) return;
397
- const agent = copilotkit.getAgent(config.agentId);
395
+ const registryAgent = copilotkit.getAgent(config.agentId);
396
+ // Prefer the per-thread clone so that state changes from the running agent
397
+ // (which is the clone, not the registry) trigger re-renders.
398
+ const agent =
399
+ getThreadClone(registryAgent, config.threadId) ?? registryAgent;
398
400
  if (!agent) return;
399
401
 
400
402
  const subscription = agent.subscribe({
401
403
  onStateChanged: forceUpdate,
402
404
  });
403
405
  return () => subscription.unsubscribe();
404
- }, [config?.agentId, copilotkit, forceUpdate]);
406
+ }, [config?.agentId, config?.threadId, copilotkit, forceUpdate]);
405
407
 
406
408
  // Subscribe to interrupt element changes for in-chat rendering.
407
409
  const [interruptElement, setInterruptElement] =
@@ -606,23 +608,6 @@ export function CopilotChatMessageView({
606
608
  );
607
609
  }
608
610
 
609
- // Auto-mount the IntelligenceIndicator on assistant message slots
610
- // when the runtime is in intelligence mode. The component self-gates
611
- // further (latest matching-assistant slot, pending tool-call grace
612
- // window) so only one pill renders at a time — mounting only for
613
- // assistant messages avoids the per-slot `useAgent` subscription
614
- // and four effects on user/reasoning/activity slots that would just
615
- // return null at the role gate anyway.
616
- if (copilotkit.intelligence !== undefined && message.role === "assistant") {
617
- elements.push(
618
- <IntelligenceIndicator
619
- key={`${message.id}-intelligence`}
620
- message={message}
621
- agentId={config?.agentId ?? DEFAULT_AGENT_ID}
622
- />,
623
- );
624
- }
625
-
626
611
  return elements.filter(Boolean) as React.ReactElement[];
627
612
  };
628
613
 
@@ -89,8 +89,8 @@ export type CopilotChatViewProps = WithSlots<
89
89
  /**
90
90
  * When `true`, suppresses the welcome screen while a thread's initial
91
91
  * connect is in flight. Prevents the "How can I help you today?" flash
92
- * that would otherwise appear between mounting an empty agent instance
93
- * and the bootstrap messages arriving from /connect.
92
+ * that would otherwise appear between mounting an empty cloned agent and
93
+ * the bootstrap messages arriving from /connect.
94
94
  */
95
95
  isConnecting?: boolean;
96
96
  /**
@@ -9,7 +9,8 @@ import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
9
9
 
10
10
  /**
11
11
  * Mock agent that records every connectAgent() invocation and resolves
12
- * immediately with an empty run result.
12
+ * immediately with an empty run result. Tracking lives on the class so
13
+ * per-thread clones (from useAgent's WeakMap) share the counter.
13
14
  */
14
15
  class TrackingAgent extends MockStepwiseAgent {
15
16
  static connectCalls: Array<{
@@ -106,6 +107,7 @@ describe("CopilotChat welcome / connect integration", () => {
106
107
  expect(TrackingAgent.connectCalls.length).toBeGreaterThan(0);
107
108
  });
108
109
 
110
+ // The per-thread clone carries threadId; agentId is the default.
109
111
  expect(
110
112
  TrackingAgent.connectCalls.some((c) => c.threadId === "real-thread"),
111
113
  ).toBe(true);
@@ -1,7 +1,6 @@
1
1
  import React from "react";
2
2
  import { fireEvent, render, screen, waitFor } from "@testing-library/react";
3
3
  import { z } from "zod";
4
- import type { AbstractAgent } from "@ag-ui/client";
5
4
  import { EventType } from "@ag-ui/client";
6
5
  import {
7
6
  MockReconnectableAgent,
@@ -18,7 +17,9 @@ import {
18
17
  CopilotKitProvider,
19
18
  useCopilotKit,
20
19
  } from "../../../providers";
20
+ import type { AbstractAgent } from "@ag-ui/client";
21
21
  import { IntelligenceAgent } from "@copilotkit/core";
22
+ import { getThreadClone } from "../../../hooks/use-agent";
22
23
  import { createA2UIMessageRenderer } from "../../../a2ui/A2UIMessageRenderer";
23
24
  import type { Theme } from "@copilotkit/a2ui-renderer";
24
25
  import { CopilotChat } from "..";
@@ -305,28 +306,18 @@ describe("CopilotChat activity message rendering", () => {
305
306
  expect(capturedCopilotkit).toBeDefined();
306
307
  });
307
308
 
308
- it("activity renderers receive the agent under the config agentId, not any other registered agent", async () => {
309
- // Regression: the renderer's `agent` prop must come from
310
- // `copilotkit.getAgent(config.agentId)` the local registry id from
311
- // CopilotChatConfigurationProvider not from any other agent in the
312
- // registry (e.g. a sibling chat's agent).
313
- //
314
- // The trap this catches is a refactor where the renderer pipeline keys
315
- // on something other than `config.agentId` (e.g. for proxied
316
- // registrations, accidentally keying on `proxy.runtimeAgentId`). The
317
- // assertion shape — "two agents in the registry, the renderer must
318
- // receive the one matching config.agentId" — is generic to that bug
319
- // class. The proxy-routing-specific behavior (registering a
320
- // ProxiedCopilotRuntimeAgent with `runtimeAgentId !== agentId` and
321
- // verifying `getAgent(agentId)` returns the proxy, not the agent at
322
- // runtimeAgentId) is covered by `core-register-proxied-agent.test.ts`
323
- // at the registry level.
324
- const localAgent = new MockStepwiseAgent();
325
- localAgent.agentId = "chat-1";
326
- const otherAgent = new MockStepwiseAgent();
327
- otherAgent.agentId = "default";
309
+ it("passes the per-thread clone (not the registry agent) to activity message renderers", async () => {
310
+ // Regression test for: A2UI button clicks firing runAgent on the registry
311
+ // agent instead of the per-thread clone that CopilotChat renders from.
312
+ // Caused by useRenderActivityMessage calling copilotkit.getAgent() directly
313
+ // instead of getThreadClone(registryAgent, threadId) ?? registryAgent.
314
+ const agent = new MockStepwiseAgent();
315
+ const agentId = "action-agent";
316
+ agent.agentId = agentId;
317
+ const threadId = "thread-for-action-test";
328
318
 
329
319
  let capturedAgent: AbstractAgent | undefined;
320
+
330
321
  const activityRenderer: ReactActivityMessageRenderer<{ label: string }> = {
331
322
  activityType: "button-action",
332
323
  content: z.object({ label: z.string() }),
@@ -337,9 +328,9 @@ describe("CopilotChat activity message rendering", () => {
337
328
  };
338
329
 
339
330
  renderWithCopilotKit({
340
- agents: { "chat-1": localAgent, default: otherAgent },
341
- agentId: "chat-1",
342
- threadId: "thread-for-action-test",
331
+ agents: { [agentId]: agent },
332
+ agentId,
333
+ threadId,
343
334
  renderActivityMessages: [activityRenderer],
344
335
  });
345
336
 
@@ -351,22 +342,27 @@ describe("CopilotChat activity message rendering", () => {
351
342
  expect(screen.getByText("show me buttons")).toBeDefined();
352
343
  });
353
344
 
354
- localAgent.emit(runStartedEvent());
355
- localAgent.emit(
345
+ agent.emit(runStartedEvent());
346
+ agent.emit(
356
347
  activitySnapshotEvent({
357
348
  messageId: testId("activity-action"),
358
349
  activityType: "button-action",
359
350
  content: { label: "Click Me" },
360
351
  }),
361
352
  );
362
- localAgent.emit(runFinishedEvent());
353
+ agent.emit(runFinishedEvent());
363
354
 
364
355
  await waitFor(() => {
365
356
  expect(screen.getByTestId("action-button")).toBeDefined();
366
357
  });
367
358
 
368
- expect(capturedAgent).toBe(localAgent);
369
- expect(capturedAgent).not.toBe(otherAgent);
359
+ // CopilotChat creates a per-thread clone via useAgent. The activity renderer
360
+ // must receive that clone so that handleAction → runAgent targets the same
361
+ // instance chat is rendering from.
362
+ const clone = getThreadClone(agent, threadId);
363
+ expect(clone).toBeDefined();
364
+ expect(capturedAgent).toBe(clone);
365
+ expect(capturedAgent).not.toBe(agent); // must NOT be the registry agent
370
366
  });
371
367
 
372
368
  it("restores a completed A2UI surface after reconnect from an event-native baseline", async () => {
@@ -788,6 +788,108 @@ describe("MCP Apps Activity Renderer E2E", () => {
788
788
  });
789
789
  });
790
790
 
791
+ describe("Effect Re-fire Prevention", () => {
792
+ // Helper: count proxied resources/read calls for a given URI.
793
+ const readCountFor = (agent: MockMCPProxyAgent, uri: string) =>
794
+ agent.runAgentCalls.filter(
795
+ (call) =>
796
+ call.input.forwardedProps?.__proxiedMCPRequest?.method ===
797
+ "resources/read" &&
798
+ (
799
+ call.input.forwardedProps?.__proxiedMCPRequest as {
800
+ params?: { uri?: string };
801
+ }
802
+ )?.params?.uri === uri,
803
+ ).length;
804
+
805
+ it("does not refetch when content reference changes but resourceUri/serverHash/serverId are unchanged", async () => {
806
+ // Regression: ACTIVITY_SNAPSHOT events from the MCP middleware mutate
807
+ // content.result on every chat re-render, giving `content` a new object
808
+ // identity. The fetch effect must depend on the resource identifiers
809
+ // (primitives), not the content reference, otherwise it re-fires
810
+ // mid-stream and races with `agent.messages` -> "Message not found" loop.
811
+ const agent = new MockMCPProxyAgent();
812
+ const agentId = "mcp-test-agent";
813
+ agent.agentId = agentId;
814
+
815
+ agent.setRunAgentResponse("resources/read", {
816
+ contents: [
817
+ {
818
+ uri: "ui://stable/resource",
819
+ mimeType: "text/html",
820
+ text: "<html><body>Stable</body></html>",
821
+ },
822
+ ],
823
+ });
824
+
825
+ renderWithCopilotKit({
826
+ agents: { [agentId]: agent },
827
+ agentId,
828
+ });
829
+
830
+ const input = await screen.findByRole("textbox");
831
+ fireEvent.change(input, { target: { value: "Stable" } });
832
+ fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
833
+
834
+ await waitFor(() => {
835
+ expect(screen.getByText("Stable")).toBeDefined();
836
+ });
837
+
838
+ const messageId = testId("mcp-stable");
839
+
840
+ // First snapshot: initial mount triggers a fetch.
841
+ agent.emit(runStartedEvent());
842
+ agent.emit(
843
+ activitySnapshotEvent({
844
+ messageId,
845
+ activityType: MCPAppsActivityType,
846
+ content: mcpAppsActivityContent({
847
+ resourceUri: "ui://stable/resource",
848
+ serverHash: "stable-hash",
849
+ result: {
850
+ content: [{ type: "text", text: "first" }],
851
+ isError: false,
852
+ },
853
+ }),
854
+ }),
855
+ );
856
+ agent.emit(runFinishedEvent());
857
+
858
+ await waitFor(
859
+ () => {
860
+ expect(readCountFor(agent, "ui://stable/resource")).toBe(1);
861
+ },
862
+ { timeout: 2000 },
863
+ );
864
+
865
+ // Second snapshot: same identifiers, fresh content reference (mutated
866
+ // result). Should NOT refire the fetch effect.
867
+ agent.emit(runStartedEvent());
868
+ agent.emit(
869
+ activitySnapshotEvent({
870
+ messageId,
871
+ activityType: MCPAppsActivityType,
872
+ content: mcpAppsActivityContent({
873
+ resourceUri: "ui://stable/resource",
874
+ serverHash: "stable-hash",
875
+ result: {
876
+ content: [{ type: "text", text: "second" }],
877
+ isError: false,
878
+ },
879
+ }),
880
+ }),
881
+ );
882
+ agent.emit(runFinishedEvent());
883
+
884
+ // Give any incorrect re-fire a chance to land before asserting.
885
+ await act(async () => {
886
+ await new Promise((resolve) => setTimeout(resolve, 100));
887
+ });
888
+
889
+ expect(readCountFor(agent, "ui://stable/resource")).toBe(1);
890
+ });
891
+ });
892
+
791
893
  describe("Content Types", () => {
792
894
  it("handles text content from resource", async () => {
793
895
  const agent = new MockMCPProxyAgent();
@@ -49,11 +49,6 @@ class MockMCPProxyAgent extends AbstractAgent {
49
49
  this.runAgentResponses.set(method, response);
50
50
  }
51
51
 
52
- addMessage(msg: Parameters<AbstractAgent["addMessage"]>[0]) {
53
- this.addMessageCalls.push(msg as any);
54
- return super.addMessage(msg);
55
- }
56
-
57
52
  emit(event: BaseEvent) {
58
53
  if (event.type === EventType.RUN_STARTED) {
59
54
  this.isRunning = true;
@@ -75,6 +70,66 @@ class MockMCPProxyAgent extends AbstractAgent {
75
70
  });
76
71
  }
77
72
 
73
+ clone(): MockMCPProxyAgent {
74
+ const cloned = new MockMCPProxyAgent();
75
+ cloned.agentId = this.agentId;
76
+ type Internal = {
77
+ subject: Subject<BaseEvent>;
78
+ runAgentCalls: Array<{ input: Partial<RunAgentInput> }>;
79
+ addMessageCalls: Array<{ id: string; role: string; content: string }>;
80
+ runAgentResponses: Map<string, unknown>;
81
+ };
82
+ (cloned as unknown as Internal).subject = (
83
+ this as unknown as Internal
84
+ ).subject;
85
+ (cloned as unknown as Internal).runAgentCalls = (
86
+ this as unknown as Internal
87
+ ).runAgentCalls;
88
+ (cloned as unknown as Internal).addMessageCalls = (
89
+ this as unknown as Internal
90
+ ).addMessageCalls;
91
+ (cloned as unknown as Internal).runAgentResponses = (
92
+ this as unknown as Internal
93
+ ).runAgentResponses;
94
+
95
+ const registry = this;
96
+ Object.defineProperty(cloned, "isRunning", {
97
+ get() {
98
+ return registry.isRunning;
99
+ },
100
+ set(v: boolean) {
101
+ registry.isRunning = v;
102
+ },
103
+ configurable: true,
104
+ enumerable: true,
105
+ });
106
+
107
+ const proto = MockMCPProxyAgent.prototype;
108
+ cloned.runAgent = async function (
109
+ input?: Partial<RunAgentInput>,
110
+ ): Promise<RunAgentResult> {
111
+ const proxiedRequest = input?.forwardedProps?.__proxiedMCPRequest;
112
+ if (proxiedRequest) {
113
+ return registry.runAgent(input);
114
+ }
115
+ return proto.runAgent.call(cloned, input);
116
+ };
117
+
118
+ // Track addMessage calls on the clone (the component uses the clone)
119
+ const origAddMessage = cloned.addMessage.bind(cloned);
120
+ cloned.addMessage = function (msg: Parameters<typeof origAddMessage>[0]) {
121
+ registry.addMessageCalls.push(msg as any);
122
+ return origAddMessage(msg);
123
+ };
124
+
125
+ // Proxy run() calls so spies on the registry's run() see clone invocations
126
+ cloned.run = function (input: RunAgentInput): Observable<BaseEvent> {
127
+ return registry.run(input);
128
+ };
129
+
130
+ return cloned;
131
+ }
132
+
78
133
  async detachActiveRun(): Promise<void> {}
79
134
 
80
135
  run(_input: RunAgentInput): Observable<BaseEvent> {
@@ -5,4 +5,3 @@ export * from "./chat";
5
5
  export * from "./WildcardToolCallRender";
6
6
  export * from "./CopilotKitInspector";
7
7
  export * from "./MCPAppsActivityRenderer";
8
- export * from "./intelligence-indicator";
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext, useEffect, useReducer } from "react";
4
+ import { CopilotKitCoreReact } from "./lib/react-core";
5
+ import type { CopilotKitCoreReactConfig } from "./lib/react-core";
6
+ import type { LicenseContextValue } from "@copilotkit/shared";
7
+
8
+ // Re-export so headless.ts (and consumers) reference the same type declaration.
9
+ export { CopilotKitCoreReact };
10
+ export type { CopilotKitCoreReactConfig };
11
+
12
+ export interface CopilotKitContextValue {
13
+ copilotkit: CopilotKitCoreReact;
14
+ /**
15
+ * Set of tool call IDs currently being executed.
16
+ * This is tracked at the provider level to ensure tool execution events
17
+ * are captured even before child components mount.
18
+ */
19
+ executingToolCallIds: ReadonlySet<string>;
20
+ }
21
+
22
+ export const EMPTY_SET: ReadonlySet<string> = new Set();
23
+
24
+ export const CopilotKitContext = createContext<CopilotKitContextValue | null>(
25
+ null,
26
+ );
27
+
28
+ export const useCopilotKit = (): CopilotKitContextValue => {
29
+ const context = useContext(CopilotKitContext);
30
+ const [, forceUpdate] = useReducer((x: number) => x + 1, 0);
31
+
32
+ if (!context) {
33
+ throw new Error("useCopilotKit must be used within CopilotKitProvider");
34
+ }
35
+ useEffect(() => {
36
+ const subscription = context.copilotkit.subscribe({
37
+ onRuntimeConnectionStatusChanged: () => {
38
+ forceUpdate();
39
+ },
40
+ });
41
+ return () => {
42
+ subscription.unsubscribe();
43
+ };
44
+ // eslint-disable-next-line react-hooks/exhaustive-deps
45
+ }, []);
46
+
47
+ return context;
48
+ };
49
+
50
+ // License context — shared between web and RN providers.
51
+ // Default is permissive (all features allowed) — providers override via createLicenseContextValue.
52
+ // Inlined here to avoid a runtime import from @copilotkit/shared, which pulls in
53
+ // Node-only deps (jose) that break React Native's Metro bundler.
54
+ export const LicenseContext = createContext<LicenseContextValue>({
55
+ status: null,
56
+ license: null,
57
+ checkFeature: () => true,
58
+ getLimit: () => null,
59
+ } as LicenseContextValue);
60
+
61
+ export const useLicenseContext = (): LicenseContextValue =>
62
+ useContext(LicenseContext);
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Headless (platform-agnostic) exports from @copilotkit/react-core/v2.
3
+ *
4
+ * No CSS, no web UI components, no DOM dependencies.
5
+ * Used by @copilotkit/react-native.
6
+ */
7
+
8
+ // Re-export from context (which is external in this build) so the .d.ts
9
+ // references the same type declaration. This avoids a nominal type mismatch
10
+ // caused by private class members being declared in two separate .d.ts files.
11
+ export { CopilotKitCoreReact } from "./context";
12
+ export type { CopilotKitCoreReactConfig } from "./context";
13
+
14
+ // Chat configuration provider (no UI, just context)
15
+ export {
16
+ CopilotChatConfigurationProvider,
17
+ useCopilotChatConfiguration,
18
+ CopilotChatDefaultLabels,
19
+ type CopilotChatLabels,
20
+ type CopilotChatConfigurationValue,
21
+ type CopilotChatConfigurationProviderProps,
22
+ } from "./providers/CopilotChatConfigurationProvider";
23
+
24
+ // Platform-agnostic hooks
25
+ export { useAgent, type UseAgentUpdate } from "./hooks/use-agent";
26
+ export { useFrontendTool } from "./hooks/use-frontend-tool";
27
+ export { useComponent } from "./hooks/use-component";
28
+ export { useHumanInTheLoop } from "./hooks/use-human-in-the-loop";
29
+ export { useInterrupt, type UseInterruptConfig } from "./hooks/use-interrupt";
30
+ export { useSuggestions } from "./hooks/use-suggestions";
31
+ export { useConfigureSuggestions } from "./hooks/use-configure-suggestions";
32
+ export {
33
+ useAgentContext,
34
+ type AgentContextInput,
35
+ type JsonSerializable,
36
+ } from "./hooks/use-agent-context";
37
+ export {
38
+ useThreads,
39
+ type Thread,
40
+ type UseThreadsInput,
41
+ type UseThreadsResult,
42
+ } from "./hooks/use-threads";
@@ -6,12 +6,12 @@ import * as v from "valibot";
6
6
  import { type } from "arktype";
7
7
  import { useRenderTool, type RenderToolProps } from "../use-render-tool";
8
8
  import { useComponent } from "../use-component";
9
- import { useCopilotKit } from "../../providers/CopilotKitProvider";
9
+ import { useCopilotKit } from "../../context";
10
10
  import { useFrontendTool } from "../use-frontend-tool";
11
11
  import type { ReactToolCallRenderer } from "../../types/react-tool-call-renderer";
12
12
  import type { StandardSchemaV1 } from "@copilotkit/shared";
13
13
 
14
- vi.mock("../../providers/CopilotKitProvider", () => ({
14
+ vi.mock("../../context", () => ({
15
15
  useCopilotKit: vi.fn(),
16
16
  }));
17
17
 
@@ -2,10 +2,10 @@ import React, { useState } from "react";
2
2
  import { render, act } from "@testing-library/react";
3
3
  import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
4
4
  import { useAgentContext, type AgentContextInput } from "../use-agent-context";
5
- import { useCopilotKit } from "../../providers/CopilotKitProvider";
5
+ import { useCopilotKit } from "../../context";
6
6
 
7
- // Mock the CopilotKitProvider
8
- vi.mock("../../providers/CopilotKitProvider", () => ({
7
+ // Mock the CopilotKit context
8
+ vi.mock("../../context", () => ({
9
9
  useCopilotKit: vi.fn(),
10
10
  }));
11
11