@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copilotkit/react-core",
3
- "version": "1.56.5-canary.1777972218",
3
+ "version": "1.57.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "ai",
@@ -73,11 +73,11 @@
73
73
  "untruncate-json": "^0.0.1",
74
74
  "use-stick-to-bottom": "^1.1.1",
75
75
  "zod-to-json-schema": "^3.24.5",
76
- "@copilotkit/core": "1.56.5-canary.1777972218",
77
- "@copilotkit/a2ui-renderer": "1.56.5-canary.1777972218",
78
- "@copilotkit/runtime-client-gql": "1.56.5-canary.1777972218",
79
- "@copilotkit/shared": "1.56.5-canary.1777972218",
80
- "@copilotkit/web-inspector": "1.56.5-canary.1777972218"
76
+ "@copilotkit/core": "1.57.0",
77
+ "@copilotkit/shared": "1.57.0",
78
+ "@copilotkit/a2ui-renderer": "1.57.0",
79
+ "@copilotkit/runtime-client-gql": "1.57.0",
80
+ "@copilotkit/web-inspector": "1.57.0"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@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,
@@ -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 });
@@ -22,10 +22,9 @@ import {
22
22
  } from "@ag-ui/core";
23
23
  import { twMerge } from "tailwind-merge";
24
24
  import { useRenderActivityMessage, useRenderCustomMessages } from "../../hooks";
25
+ import { getThreadClone } from "../../hooks/use-agent";
25
26
  import { useCopilotKit } from "../../providers/CopilotKitProvider";
26
27
  import { useCopilotChatConfiguration } from "../../providers/CopilotChatConfigurationProvider";
27
- import { IntelligenceIndicator } from "../intelligence-indicator";
28
- import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
29
28
 
30
29
  /**
31
30
  * Resolves a slot value into a { Component, slotProps } pair, handling the three
@@ -283,63 +282,25 @@ const MemoizedCustomMessage = React.memo(
283
282
  message: Message;
284
283
  position: "before" | "after";
285
284
  }) => React.ReactElement | null;
286
- stateSnapshot: unknown;
287
- numberOfMessagesInRun: number | undefined;
288
- isInLatestRun: boolean | undefined;
289
- isRunning: boolean;
285
+ stateSnapshot?: unknown;
290
286
  }) {
291
287
  return renderCustomMessage({ message, position });
292
288
  },
293
289
  (prevProps, nextProps) => {
294
- // Skip the re-render unless an input that an authored renderer can
295
- // observe has changed. Each branch returns false to invalidate.
290
+ // Only re-render if the message or position changed
296
291
  if (prevProps.message.id !== nextProps.message.id) return false;
297
292
  if (prevProps.position !== nextProps.position) return false;
293
+ // Compare message content - for assistant messages this is a string, for others may differ
298
294
  if (prevProps.message.content !== nextProps.message.content) return false;
299
295
  if (prevProps.message.role !== nextProps.message.role) return false;
300
- // State snapshot custom renderers may read it. Deep-compare via
301
- // JSON.stringify (state-manager already deep-copies on each call, so
302
- // reference equality would never hit anyway).
296
+ // Compare state snapshot - custom renderers may depend on state
303
297
  if (
304
298
  JSON.stringify(prevProps.stateSnapshot) !==
305
299
  JSON.stringify(nextProps.stateSnapshot)
306
300
  )
307
301
  return false;
308
- // Re-render when this message's run grows. The runId-to-message
309
- // association is unidirectional in StateManager (only set, never
310
- // revoked except via clearAgentState/clearThreadState which wipe
311
- // wholesale), so in practice this fires only on growth as new messages
312
- // stream into the run. Slots in completed runs are unaffected — their
313
- // count is stable once the run has finished.
314
- if (prevProps.numberOfMessagesInRun !== nextProps.numberOfMessagesInRun)
315
- return false;
316
- // Re-render when this message's run stops being the latest run on the
317
- // thread — e.g. a new run starts and previously-completed messages need
318
- // to drop "is this the latest activity?" indicators. Only the slots
319
- // belonging to the run that just lost its "latest" status are
320
- // invalidated; the new run's slots and any older runs are unaffected.
321
- if (prevProps.isInLatestRun !== nextProps.isInLatestRun) return false;
322
- // Re-render when isRunning flips, but only for slots known to belong
323
- // to the latest run (`isInLatestRun === true`). Slots whose run is
324
- // older or whose run-id hasn't been recorded yet (undefined) are not
325
- // invalidated on isRunning changes — this preserves the perf guarantee
326
- // that completed messages skip re-renders during streaming.
327
- //
328
- // Renderers that gate solely on `agent.isRunning` and attach to a
329
- // message added before any run started (e.g. a user message inserted
330
- // before runAgent fires) won't observe the false→true transition for
331
- // the first run. In practice this is rare; the canonical pattern is
332
- // to gate on the slot's own runId membership (see
333
- // intelligence-indicator/__tests__/IntelligenceIndicator.e2e.test.tsx
334
- // for the recommended renderer shape).
335
- if (
336
- nextProps.isInLatestRun &&
337
- prevProps.isRunning !== nextProps.isRunning
338
- )
339
- return false;
340
- // Note: renderCustomMessage's reference is intentionally not compared —
341
- // it's a closure that changes frequently. The inputs above are the only
342
- // observable signals.
302
+ // Note: We don't compare renderCustomMessage function reference because it changes
303
+ // frequently. The message and state comparison is sufficient to determine if a re-render is needed.
343
304
  return true;
344
305
  },
345
306
  );
@@ -431,14 +392,18 @@ export function CopilotChatMessageView({
431
392
  // Subscribe to state changes so custom message renderers re-render when state updates.
432
393
  useEffect(() => {
433
394
  if (!config?.agentId) return;
434
- 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;
435
400
  if (!agent) return;
436
401
 
437
402
  const subscription = agent.subscribe({
438
403
  onStateChanged: forceUpdate,
439
404
  });
440
405
  return () => subscription.unsubscribe();
441
- }, [config?.agentId, copilotkit, forceUpdate]);
406
+ }, [config?.agentId, config?.threadId, copilotkit, forceUpdate]);
442
407
 
443
408
  // Subscribe to interrupt element changes for in-chat rendering.
444
409
  const [interruptElement, setInterruptElement] =
@@ -453,48 +418,6 @@ export function CopilotChatMessageView({
453
418
  return () => subscription.unsubscribe();
454
419
  }, [copilotkit]);
455
420
 
456
- // Build per-run metadata once per render, not once per message. The two
457
- // memo inputs MemoizedCustomMessage cares about (numberOfMessagesInRun and
458
- // isInLatestRun) are both derived from the same scan of `messages` →
459
- // `getRunIdForMessage`, so consolidating here turns an O(n²) per-render
460
- // pass (one O(n) helper call per message) into a single O(n) pass.
461
- //
462
- // Re-fires whenever messages, the active agent/thread, or copilotkit
463
- // changes. The internal messageToRun map can update without these props
464
- // changing (a runId association lands one tick after the message is
465
- // appended) — in that case the next external trigger refreshes. Note
466
- // the `MemoizedCustomMessage` gate at the `isInLatestRun &&` check is
467
- // strict-truthy — slots whose runId is still undefined during that
468
- // pre-association window will not re-render on isRunning flips. The
469
- // canonical pattern (see the IntelligenceIndicator e2e test) is to gate
470
- // the renderer on the slot's own runId membership rather than `isRunning`
471
- // alone, so the limitation doesn't bite typical use.
472
- const runMetadata = useMemo(() => {
473
- if (!config) return null;
474
- const runIdByMessageId = new Map<string, string>();
475
- for (const msg of messages) {
476
- const r = copilotkit.getRunIdForMessage(
477
- config.agentId,
478
- config.threadId,
479
- msg.id,
480
- );
481
- if (r) runIdByMessageId.set(msg.id, r);
482
- }
483
- const countByRunId = new Map<string, number>();
484
- for (const r of runIdByMessageId.values()) {
485
- countByRunId.set(r, (countByRunId.get(r) ?? 0) + 1);
486
- }
487
- let latestRunId: string | undefined;
488
- for (let i = messages.length - 1; i >= 0; i--) {
489
- const r = runIdByMessageId.get(messages[i]!.id);
490
- if (r) {
491
- latestRunId = r;
492
- break;
493
- }
494
- }
495
- return { runIdByMessageId, countByRunId, latestRunId };
496
- }, [messages, config?.agentId, config?.threadId, copilotkit]);
497
-
498
421
  // Helper to get state snapshot for a message (used for memoization)
499
422
  const getStateSnapshotForMessage = (messageId: string): unknown => {
500
423
  if (!config) return undefined;
@@ -619,14 +542,6 @@ export function CopilotChatMessageView({
619
542
  const renderMessageBlock = (message: Message): React.ReactElement[] => {
620
543
  const elements: (React.ReactElement | null | undefined)[] = [];
621
544
  const stateSnapshot = getStateSnapshotForMessage(message.id);
622
- const messageRunId = runMetadata?.runIdByMessageId.get(message.id);
623
- const numberOfMessagesInRun = messageRunId
624
- ? runMetadata?.countByRunId.get(messageRunId)
625
- : undefined;
626
- const isInLatestRun =
627
- messageRunId === undefined
628
- ? undefined
629
- : messageRunId === runMetadata?.latestRunId;
630
545
 
631
546
  if (renderCustomMessage) {
632
547
  elements.push(
@@ -636,9 +551,6 @@ export function CopilotChatMessageView({
636
551
  position="before"
637
552
  renderCustomMessage={renderCustomMessage}
638
553
  stateSnapshot={stateSnapshot}
639
- numberOfMessagesInRun={numberOfMessagesInRun}
640
- isInLatestRun={isInLatestRun}
641
- isRunning={isRunning}
642
554
  />,
643
555
  );
644
556
  }
@@ -692,29 +604,6 @@ export function CopilotChatMessageView({
692
604
  position="after"
693
605
  renderCustomMessage={renderCustomMessage}
694
606
  stateSnapshot={stateSnapshot}
695
- numberOfMessagesInRun={numberOfMessagesInRun}
696
- isInLatestRun={isInLatestRun}
697
- isRunning={isRunning}
698
- />,
699
- );
700
- }
701
-
702
- // Auto-mount the IntelligenceIndicator on assistant message slots
703
- // when the runtime is in intelligence mode. The component self-gates
704
- // further (last in run, latest run, matching tool call) so only one
705
- // pill renders at a time — but mounting it only for assistant
706
- // messages avoids spinning up a `useAgent` subscription, a 200 ms
707
- // poll interval, and four effects on every user/reasoning/activity
708
- // slot just to have it return null at the role gate.
709
- if (
710
- copilotkit.intelligence !== undefined &&
711
- message.role === "assistant"
712
- ) {
713
- elements.push(
714
- <IntelligenceIndicator
715
- key={`${message.id}-intelligence`}
716
- message={message}
717
- agentId={config?.agentId ?? DEFAULT_AGENT_ID}
718
607
  />,
719
608
  );
720
609
  }
@@ -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.remoteAgentId`). 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 `remoteAgentId !== agentId` and
321
- // verifying `getAgent(agentId)` returns the proxy, not the agent at
322
- // remoteAgentId) 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 () => {
@@ -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";