@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.
- package/dist/{copilotkit-B_k0HSNz.cjs → copilotkit-BLlkMAjx.cjs} +368 -502
- package/dist/copilotkit-BLlkMAjx.cjs.map +1 -0
- package/dist/{copilotkit-BNlJq5UO.d.mts → copilotkit-BYnbIBN5.d.mts} +10 -62
- package/dist/copilotkit-BYnbIBN5.d.mts.map +1 -0
- package/dist/{copilotkit-ak8sGvQr.mjs → copilotkit-N0YiBG5S.mjs} +369 -497
- package/dist/copilotkit-N0YiBG5S.mjs.map +1 -0
- package/dist/{copilotkit-DgC5oCFO.d.cts → copilotkit-vx_R9p-O.d.cts} +10 -62
- package/dist/copilotkit-vx_R9p-O.d.cts.map +1 -0
- package/dist/index.cjs +5 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +5 -2
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +409 -347
- package/dist/index.umd.js.map +1 -1
- package/dist/v2/context.cjs +135 -0
- package/dist/v2/context.cjs.map +1 -0
- package/dist/v2/context.d.cts +148 -0
- package/dist/v2/context.d.cts.map +1 -0
- package/dist/v2/context.d.mts +148 -0
- package/dist/v2/context.d.mts.map +1 -0
- package/dist/v2/context.mjs +129 -0
- package/dist/v2/context.mjs.map +1 -0
- package/dist/v2/headless.cjs +1085 -0
- package/dist/v2/headless.cjs.map +1 -0
- package/dist/v2/headless.d.cts +607 -0
- package/dist/v2/headless.d.cts.map +1 -0
- package/dist/v2/headless.d.mts +514 -0
- package/dist/v2/headless.d.mts.map +1 -0
- package/dist/v2/headless.mjs +1039 -0
- package/dist/v2/headless.mjs.map +1 -0
- package/dist/v2/index.cjs +1 -2
- package/dist/v2/index.css +1 -1
- package/dist/v2/index.d.cts +2 -2
- package/dist/v2/index.d.mts +2 -2
- package/dist/v2/index.mjs +2 -2
- package/dist/v2/index.umd.js +387 -523
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +14 -6
- package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +6 -5
- package/src/hooks/use-copilot-chat_internal.ts +1 -0
- package/src/v2/components/MCPAppsActivityRenderer.tsx +9 -3
- package/src/v2/components/chat/CopilotChat.tsx +1 -2
- package/src/v2/components/chat/CopilotChatMessageView.tsx +9 -24
- package/src/v2/components/chat/CopilotChatView.tsx +2 -2
- package/src/v2/components/chat/__tests__/CopilotChat.welcomeGate.test.tsx +3 -1
- package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +25 -29
- package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +102 -0
- package/src/v2/components/chat/__tests__/MCPAppsUiMessage.e2e.test.tsx +60 -5
- package/src/v2/components/index.ts +0 -1
- package/src/v2/context.ts +62 -0
- package/src/v2/headless.ts +42 -0
- package/src/v2/hooks/__tests__/standard-schema.test.tsx +2 -2
- package/src/v2/hooks/__tests__/use-agent-context.test.tsx +3 -3
- package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +3 -3
- package/src/v2/hooks/__tests__/use-agent-thread-isolation.test.tsx +333 -0
- package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +85 -85
- package/src/v2/hooks/__tests__/use-interrupt.test.tsx +2 -2
- package/src/v2/hooks/__tests__/use-render-tool.test.tsx +2 -2
- package/src/v2/hooks/__tests__/use-threads.test.tsx +2 -2
- package/src/v2/hooks/__tests__/zod-regression.test.tsx +2 -2
- package/src/v2/hooks/use-agent-context.tsx +1 -1
- package/src/v2/hooks/use-agent.tsx +118 -9
- package/src/v2/hooks/use-configure-suggestions.tsx +1 -1
- package/src/v2/hooks/use-frontend-tool.tsx +2 -2
- package/src/v2/hooks/use-human-in-the-loop.tsx +1 -1
- package/src/v2/hooks/use-interrupt.tsx +1 -1
- package/src/v2/hooks/use-render-activity-message.tsx +11 -3
- package/src/v2/hooks/use-render-custom-messages.tsx +6 -1
- package/src/v2/hooks/use-render-tool-call.tsx +1 -1
- package/src/v2/hooks/use-render-tool.tsx +2 -2
- package/src/v2/hooks/use-suggestions.tsx +1 -1
- package/src/v2/hooks/use-threads.tsx +1 -1
- package/src/v2/providers/CopilotKitProvider.tsx +19 -59
- package/src/v2/styles/globals.css +0 -118
- package/tsdown.config.ts +75 -0
- package/dist/copilotkit-BNlJq5UO.d.mts.map +0 -1
- package/dist/copilotkit-B_k0HSNz.cjs.map +0 -1
- package/dist/copilotkit-DgC5oCFO.d.cts.map +0 -1
- package/dist/copilotkit-ak8sGvQr.mjs.map +0 -1
- package/src/v2/components/intelligence-indicator/IntelligenceIndicator.tsx +0 -286
- package/src/v2/components/intelligence-indicator/__tests__/IntelligenceIndicator.e2e.test.tsx +0 -464
- 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.
|
|
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.
|
|
77
|
-
"@copilotkit/
|
|
78
|
-
"@copilotkit/
|
|
79
|
-
"@copilotkit/
|
|
80
|
-
"@copilotkit/
|
|
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
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
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
|
|
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({
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
93
|
-
*
|
|
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("
|
|
309
|
-
// Regression:
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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: {
|
|
341
|
-
agentId
|
|
342
|
-
threadId
|
|
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
|
-
|
|
355
|
-
|
|
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
|
-
|
|
353
|
+
agent.emit(runFinishedEvent());
|
|
363
354
|
|
|
364
355
|
await waitFor(() => {
|
|
365
356
|
expect(screen.getByTestId("action-button")).toBeDefined();
|
|
366
357
|
});
|
|
367
358
|
|
|
368
|
-
|
|
369
|
-
|
|
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> {
|
|
@@ -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 "../../
|
|
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("../../
|
|
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 "../../
|
|
5
|
+
import { useCopilotKit } from "../../context";
|
|
6
6
|
|
|
7
|
-
// Mock the
|
|
8
|
-
vi.mock("../../
|
|
7
|
+
// Mock the CopilotKit context
|
|
8
|
+
vi.mock("../../context", () => ({
|
|
9
9
|
useCopilotKit: vi.fn(),
|
|
10
10
|
}));
|
|
11
11
|
|