@copilotkit/react-core 1.55.3-canary.1776243725 → 1.55.3-canary.1776979102

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 (58) hide show
  1. package/dist/{copilotkit-opur-20s.d.mts → copilotkit-3mXoM0Hd.d.mts} +9 -29
  2. package/dist/copilotkit-3mXoM0Hd.d.mts.map +1 -0
  3. package/dist/{copilotkit-EfopO2gn.d.cts → copilotkit-BDDjvB-p.d.cts} +9 -29
  4. package/dist/copilotkit-BDDjvB-p.d.cts.map +1 -0
  5. package/dist/{copilotkit-BoOnQHlE.cjs → copilotkit-BkcqmpWt.cjs} +162 -280
  6. package/dist/copilotkit-BkcqmpWt.cjs.map +1 -0
  7. package/dist/{copilotkit-Bm4ox8G0.mjs → copilotkit-C7n8Umv9.mjs} +164 -276
  8. package/dist/copilotkit-C7n8Umv9.mjs.map +1 -0
  9. package/dist/index.cjs +4 -9
  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 +4 -9
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/index.umd.js +143 -230
  16. package/dist/index.umd.js.map +1 -1
  17. package/dist/v2/index.cjs +1 -2
  18. package/dist/v2/index.d.cts +2 -2
  19. package/dist/v2/index.d.mts +2 -2
  20. package/dist/v2/index.mjs +2 -2
  21. package/dist/v2/index.umd.js +165 -279
  22. package/dist/v2/index.umd.js.map +1 -1
  23. package/package.json +6 -6
  24. package/src/components/copilot-provider/copilot-messages.tsx +24 -39
  25. package/src/components/copilot-provider/copilotkit-props.tsx +5 -9
  26. package/src/components/copilot-provider/copilotkit.tsx +1 -4
  27. package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +16 -27
  28. package/src/hooks/use-copilot-chat_internal.ts +4 -15
  29. package/src/v2/__tests__/utils/test-helpers.tsx +7 -40
  30. package/src/v2/components/chat/CopilotChat.tsx +1 -1
  31. package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +15 -18
  32. package/src/v2/components/chat/CopilotChatMessageView.tsx +2 -7
  33. package/src/v2/components/chat/CopilotChatReasoningMessage.tsx +4 -17
  34. package/src/v2/components/chat/CopilotChatUserMessage.tsx +10 -13
  35. package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.tsx +5 -131
  36. package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +0 -60
  37. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.tsx +1 -1
  38. package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.tsx +2 -5
  39. package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +2 -5
  40. package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +1 -55
  41. package/src/v2/hooks/__tests__/use-agent-context-timing.e2e.test.tsx +0 -8
  42. package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +10 -10
  43. package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +2 -13
  44. package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +4 -23
  45. package/src/v2/hooks/index.ts +0 -1
  46. package/src/v2/hooks/use-agent.tsx +10 -157
  47. package/src/v2/hooks/use-render-activity-message.tsx +3 -9
  48. package/src/v2/hooks/use-render-custom-messages.tsx +1 -6
  49. package/src/v2/providers/CopilotKitProvider.tsx +2 -6
  50. package/dist/copilotkit-Bm4ox8G0.mjs.map +0 -1
  51. package/dist/copilotkit-BoOnQHlE.cjs.map +0 -1
  52. package/dist/copilotkit-EfopO2gn.d.cts.map +0 -1
  53. package/dist/copilotkit-opur-20s.d.mts.map +0 -1
  54. package/src/components/copilot-provider/__tests__/error-visibility-prod.test.tsx +0 -70
  55. package/src/v2/components/chat/__tests__/CopilotChatCopyButton.clipboard.test.tsx +0 -241
  56. package/src/v2/hooks/__tests__/use-agent-thread-isolation.test.tsx +0 -327
  57. package/src/v2/hooks/__tests__/use-capabilities.test.tsx +0 -76
  58. package/src/v2/hooks/use-capabilities.tsx +0 -25
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copilotkit/react-core",
3
- "version": "1.55.3-canary.1776243725",
3
+ "version": "1.55.3-canary.1776979102",
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/a2ui-renderer": "1.55.3-canary.1776243725",
77
- "@copilotkit/core": "1.55.3-canary.1776243725",
78
- "@copilotkit/shared": "1.55.3-canary.1776243725",
79
- "@copilotkit/runtime-client-gql": "1.55.3-canary.1776243725",
80
- "@copilotkit/web-inspector": "1.55.3-canary.1776243725"
76
+ "@copilotkit/a2ui-renderer": "1.55.3-canary.1776979102",
77
+ "@copilotkit/core": "1.55.3-canary.1776979102",
78
+ "@copilotkit/runtime-client-gql": "1.55.3-canary.1776979102",
79
+ "@copilotkit/shared": "1.55.3-canary.1776979102",
80
+ "@copilotkit/web-inspector": "1.55.3-canary.1776979102"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@tailwindcss/cli": "^4.1.11",
@@ -31,33 +31,6 @@ import {
31
31
  } from "@copilotkit/shared";
32
32
  import { Suggestion } from "@copilotkit/core";
33
33
 
34
- /**
35
- * Determine whether a GraphQL error should be suppressed based on its visibility
36
- * and whether the dev console is active.
37
- *
38
- * Returns `null` when the error should be surfaced to the UI, or a log prefix
39
- * string when the error should be suppressed (logged to console only).
40
- *
41
- * Exported for unit testing.
42
- */
43
- export function getErrorSuppression(
44
- visibility: ErrorVisibility | undefined,
45
- isDev: boolean,
46
- ): string | null {
47
- // Silent errors are always suppressed
48
- if (visibility === ErrorVisibility.SILENT) {
49
- return "CopilotKit Silent Error:";
50
- }
51
-
52
- // DEV_ONLY errors are suppressed in production
53
- if (!isDev && visibility === ErrorVisibility.DEV_ONLY) {
54
- return "CopilotKit Error (hidden in production):";
55
- }
56
-
57
- // All other visibilities (TOAST, BANNER, undefined) are always surfaced
58
- return null;
59
- }
60
-
61
34
  // Helper to determine if error should show as banner based on visibility and legacy patterns
62
35
  function shouldShowAsBanner(gqlError: GraphQLError): boolean {
63
36
  const extensions = gqlError.extensions;
@@ -251,13 +224,20 @@ export function CopilotMessages({ children }: { children: ReactNode }) {
251
224
  const visibility = extensions?.visibility as ErrorVisibility;
252
225
  const isDev = shouldShowDevConsole(showDevConsole);
253
226
 
254
- const suppression = getErrorSuppression(visibility, isDev);
255
- if (suppression) {
256
- console.error(suppression, gqlError.message);
227
+ if (!isDev) {
228
+ console.error(
229
+ "CopilotKit Error (hidden in production):",
230
+ gqlError.message,
231
+ );
232
+ return;
233
+ }
234
+
235
+ // Silent errors - just log
236
+ if (visibility === ErrorVisibility.SILENT) {
237
+ console.error("CopilotKit Silent Error:", gqlError.message);
257
238
  return;
258
239
  }
259
240
 
260
- // TOAST and BANNER errors are always surfaced, even in production
261
241
  // All other errors (including DEV_ONLY) show as banners for consistency
262
242
  const ckError = createStructuredError(gqlError);
263
243
  if (ckError) {
@@ -279,14 +259,19 @@ export function CopilotMessages({ children }: { children: ReactNode }) {
279
259
  // Process all errors as banners
280
260
  graphQLErrors.forEach(routeError);
281
261
  } else {
282
- // Non-GraphQL errors are always surfaced to the user
283
- const fallbackError = new CopilotKitError({
284
- message: error?.message || String(error),
285
- code: CopilotKitErrorCode.UNKNOWN,
286
- });
287
- setBannerError(fallbackError);
288
- // Trace the non-GraphQL error
289
- traceUIError(fallbackError, error);
262
+ const isDev = shouldShowDevConsole(showDevConsole);
263
+ if (!isDev) {
264
+ console.error("CopilotKit Error (hidden in production):", error);
265
+ } else {
266
+ // Route non-GraphQL errors to banner as well
267
+ const fallbackError = new CopilotKitError({
268
+ message: error?.message || String(error),
269
+ code: CopilotKitErrorCode.UNKNOWN,
270
+ });
271
+ setBannerError(fallbackError);
272
+ // Trace the non-GraphQL error
273
+ traceUIError(fallbackError, error);
274
+ }
290
275
  }
291
276
  },
292
277
  [setBannerError, showDevConsole, traceUIError],
@@ -68,19 +68,15 @@ export interface CopilotKitProps extends Omit<
68
68
 
69
69
  /**
70
70
  * Additional headers to be sent with the request.
71
- * Can be a static object or a function that returns headers dynamically
72
- * (useful for refreshing auth tokens).
73
71
  *
74
72
  * For example:
75
- * ```tsx
76
- * // Static headers
77
- * headers={{ "Authorization": "Bearer X" }}
78
- *
79
- * // Dynamic headers (re-evaluated on each render)
80
- * headers={() => ({ "Authorization": `Bearer ${getToken()}` })}
73
+ * ```json
74
+ * {
75
+ * "Authorization": "Bearer X"
76
+ * }
81
77
  * ```
82
78
  */
83
- headers?: Record<string, string> | (() => Record<string, string>);
79
+ headers?: Record<string, string>;
84
80
 
85
81
  /**
86
82
  * The children to be rendered within the CopilotKit.
@@ -311,10 +311,7 @@ export function CopilotKitInternal(cpkProps: CopilotKitProps) {
311
311
  publicApiKey: publicApiKey,
312
312
  ...(cloud ? { cloud } : {}),
313
313
  chatApiEndpoint: chatApiEndpoint,
314
- headers:
315
- typeof props.headers === "function"
316
- ? props.headers()
317
- : props.headers || {},
314
+ headers: props.headers || {},
318
315
  properties: props.properties || {},
319
316
  transcribeAudioUrl: props.transcribeAudioUrl,
320
317
  textToSpeechUrl: props.textToSpeechUrl,
@@ -178,32 +178,18 @@ describe("useCopilotChatInternal – connectAgent guard", () => {
178
178
  });
179
179
  });
180
180
 
181
- it("does not call connectAgent when threadId matches (same agent instance, no re-render)", async () => {
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.
181
+ it("does not call connectAgent when threadId matches agent's threadId", () => {
186
182
  mockRuntimeConnectionStatus =
187
183
  CopilotKitCoreRuntimeConnectionStatus.Connected;
188
- mockAgent.threadId = "config-thread-id";
184
+ mockAgent.threadId = "config-thread-id"; // same as mockConfigThreadId
189
185
  applyMocks();
190
186
 
191
- const { rerender } = renderHook(() => useCopilotChatInternal(), {
192
- wrapper: createWrapper(),
193
- });
194
-
195
- await vi.waitFor(() => {
196
- expect(mockConnectAgent).toHaveBeenCalledTimes(1);
197
- });
187
+ renderHook(() => useCopilotChatInternal(), { wrapper: createWrapper() });
198
188
 
199
- // Re-render with same agent — should NOT connect again
200
- rerender();
201
- await vi.waitFor(() => {
202
- expect(mockConnectAgent).toHaveBeenCalledTimes(1);
203
- });
189
+ expect(mockConnectAgent).not.toHaveBeenCalled();
204
190
  });
205
191
 
206
- it("calls connectAgent when config threadId is missing", async () => {
192
+ it("does not call connectAgent when config threadId is missing", () => {
207
193
  mockRuntimeConnectionStatus =
208
194
  CopilotKitCoreRuntimeConnectionStatus.Connected;
209
195
  mockConfigThreadId = undefined;
@@ -211,12 +197,10 @@ describe("useCopilotChatInternal – connectAgent guard", () => {
211
197
 
212
198
  renderHook(() => useCopilotChatInternal(), { wrapper: createWrapper() });
213
199
 
214
- await vi.waitFor(() => {
215
- expect(mockConnectAgent).toHaveBeenCalledTimes(1);
216
- });
200
+ expect(mockConnectAgent).not.toHaveBeenCalled();
217
201
  });
218
202
 
219
- it("calls connectAgent when status is Connected and threadIds differ", async () => {
203
+ it("calls connectAgent when all guard conditions are met", async () => {
220
204
  mockRuntimeConnectionStatus =
221
205
  CopilotKitCoreRuntimeConnectionStatus.Connected;
222
206
  mockAgent.threadId = "old-thread-id"; // differs from config
@@ -230,13 +214,18 @@ describe("useCopilotChatInternal – connectAgent guard", () => {
230
214
  });
231
215
  });
232
216
 
233
- it("passes config threadId to useAgent", () => {
217
+ it("sets agent.threadId to config threadId before calling connectAgent", async () => {
218
+ mockRuntimeConnectionStatus =
219
+ CopilotKitCoreRuntimeConnectionStatus.Connected;
220
+ mockAgent.threadId = "old-thread-id";
234
221
  applyMocks();
235
222
 
236
223
  renderHook(() => useCopilotChatInternal(), { wrapper: createWrapper() });
237
224
 
238
- expect(vi.mocked(useAgent)).toHaveBeenCalledWith(
239
- expect.objectContaining({ threadId: "config-thread-id" }),
240
- );
225
+ await vi.waitFor(() => {
226
+ expect(mockConnectAgent).toHaveBeenCalledTimes(1);
227
+ });
228
+
229
+ expect(mockAgent.threadId).toBe("config-thread-id");
241
230
  });
242
231
  });
@@ -337,17 +337,7 @@ export function useCopilotChatInternal({
337
337
 
338
338
  // Apply priority: props > existing config > defaults
339
339
  const resolvedAgentId = existingConfig?.agentId ?? "default";
340
- const { agent } = useAgent({
341
- agentId: resolvedAgentId,
342
- threadId: existingConfig?.threadId,
343
- });
344
-
345
- // Track the last agent instance we called connect() on. Without this,
346
- // connect() fires on every render where status is Connected — including
347
- // unrelated context re-renders and StrictMode double-invocations.
348
- // The ref is reset in the cleanup so that remounts (StrictMode, real
349
- // unmount/remount) always trigger a fresh connect.
350
- const lastConnectedAgentRef = useRef<AbstractAgent | null>(null);
340
+ const { agent } = useAgent({ agentId: resolvedAgentId });
351
341
 
352
342
  useEffect(() => {
353
343
  let detached = false;
@@ -382,19 +372,18 @@ export function useCopilotChatInternal({
382
372
  };
383
373
  if (
384
374
  agent &&
385
- agent !== lastConnectedAgentRef.current &&
375
+ existingConfig?.threadId &&
376
+ agent.threadId !== existingConfig.threadId &&
386
377
  copilotkit.runtimeConnectionStatus ===
387
378
  CopilotKitCoreRuntimeConnectionStatus.Connected
388
379
  ) {
389
- lastConnectedAgentRef.current = agent;
380
+ agent.threadId = existingConfig.threadId;
390
381
  connect(agent);
391
382
  }
392
383
  return () => {
393
384
  // Abort the HTTP request and detach the active run.
394
385
  // This is critical for React StrictMode which unmounts+remounts in dev,
395
386
  // preventing duplicate /connect requests from reaching the server.
396
- // Reset the ref so remounts always trigger a fresh connect.
397
- lastConnectedAgentRef.current = null;
398
387
  detached = true;
399
388
  connectAbortController.abort();
400
389
  agent?.detachActiveRun();
@@ -51,18 +51,9 @@ export class MockStepwiseAgent extends AbstractAgent {
51
51
  });
52
52
  }
53
53
 
54
- clone(): this {
55
- // Return a new instance that shares the same subject so tests can keep
56
- // controlling events via the original reference while satisfying the
57
- // clone() contract (must return a distinct object).
58
- // Use the concrete constructor so subclasses (e.g. FailingConnectAgent)
59
- // preserve their overridden methods.
60
- const cloned = new (this
61
- .constructor as new () => MockStepwiseAgent)() as this;
62
- cloned.agentId = this.agentId;
63
- (cloned as unknown as { subject: Subject<BaseEvent> }).subject =
64
- this.subject;
65
- return cloned;
54
+ clone(): MockStepwiseAgent {
55
+ // For tests, return same instance so we can keep controlling it
56
+ return this;
66
57
  }
67
58
 
68
59
  // No-op: prevent the base class from tearing down the Subject
@@ -119,21 +110,7 @@ export class MockReconnectableAgent extends AbstractAgent {
119
110
  }
120
111
 
121
112
  clone(): MockReconnectableAgent {
122
- const cloned = new MockReconnectableAgent();
123
- cloned.agentId = this.agentId;
124
- (
125
- cloned as unknown as {
126
- subject: Subject<BaseEvent>;
127
- storedEvents: BaseEvent[];
128
- }
129
- ).subject = this.subject;
130
- (
131
- cloned as unknown as {
132
- subject: Subject<BaseEvent>;
133
- storedEvents: BaseEvent[];
134
- }
135
- ).storedEvents = this.storedEvents;
136
- return cloned;
113
+ return this;
137
114
  }
138
115
 
139
116
  // No-op: prevent the base class from tearing down the Subject
@@ -530,20 +507,10 @@ export function emitSuggestionToolCall(
530
507
  * A MockStepwiseAgent that emits suggestion events when run() is called
531
508
  */
532
509
  export class SuggestionsProviderAgent extends MockStepwiseAgent {
533
- // Shared via a container so clone() and original see the same value even
534
- // when setSuggestions() is called after the clone is created.
535
- private _shared: { suggestions: Array<{ title: string; message: string }> } =
536
- { suggestions: [] };
510
+ private _suggestions: Array<{ title: string; message: string }> = [];
537
511
 
538
512
  setSuggestions(suggestions: Array<{ title: string; message: string }>) {
539
- this._shared.suggestions = suggestions;
540
- }
541
-
542
- clone(): this {
543
- const cloned = super.clone();
544
- (cloned as unknown as { _shared: typeof this._shared })._shared =
545
- this._shared;
546
- return cloned;
513
+ this._suggestions = suggestions;
547
514
  }
548
515
 
549
516
  run(_input: RunAgentInput): Observable<BaseEvent> {
@@ -558,7 +525,7 @@ export class SuggestionsProviderAgent extends MockStepwiseAgent {
558
525
  emitSuggestionToolCall(this, {
559
526
  toolCallId: testId("tc"),
560
527
  parentMessageId: messageId,
561
- suggestions: this._shared.suggestions,
528
+ suggestions: this._suggestions,
562
529
  });
563
530
 
564
531
  this.emit({ type: EventType.RUN_FINISHED } as BaseEvent);
@@ -104,7 +104,6 @@ export function CopilotChat({
104
104
 
105
105
  const { agent } = useAgent({
106
106
  agentId: resolvedAgentId,
107
- threadId: resolvedThreadId,
108
107
  throttleMs,
109
108
  });
110
109
  const { copilotkit } = useCopilotKit();
@@ -214,6 +213,7 @@ export function CopilotChat({
214
213
  console.error("CopilotChat: connectAgent failed", error);
215
214
  }
216
215
  };
216
+ agent.threadId = resolvedThreadId;
217
217
  connect(agent);
218
218
  return () => {
219
219
  // Abort the HTTP request and detach the active run.
@@ -22,7 +22,6 @@ import {
22
22
  import { useKatexStyles } from "../../hooks/useKatexStyles";
23
23
  import { WithSlots, renderSlot } from "../../lib/slots";
24
24
  import { Streamdown } from "streamdown";
25
- import { copyToClipboard } from "@copilotkit/shared";
26
25
  import CopilotChatToolCallsView from "./CopilotChatToolCallsView";
27
26
 
28
27
  export type CopilotChatAssistantMessageProps = WithSlots<
@@ -87,9 +86,12 @@ export function CopilotChatAssistantMessage({
87
86
  {
88
87
  onClick: async () => {
89
88
  if (message.content) {
90
- return await copyToClipboard(message.content);
89
+ try {
90
+ await navigator.clipboard.writeText(message.content);
91
+ } catch (err) {
92
+ console.error("Failed to copy message:", err);
93
+ }
91
94
  }
92
- return false;
93
95
  },
94
96
  },
95
97
  );
@@ -273,23 +275,18 @@ export namespace CopilotChatAssistantMessage {
273
275
  };
274
276
  }, []);
275
277
 
276
- const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
277
- let success = false;
278
- if (onClick) {
279
- // onClick may return a boolean indicating copy success
280
- const result = await Promise.resolve(onClick(event));
281
- success = result === true;
278
+ const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
279
+ setCopied(true);
280
+ if (timerRef.current !== null) {
281
+ clearTimeout(timerRef.current);
282
282
  }
283
+ timerRef.current = setTimeout(() => {
284
+ timerRef.current = null;
285
+ setCopied(false);
286
+ }, 2000);
283
287
 
284
- if (success) {
285
- setCopied(true);
286
- if (timerRef.current !== null) {
287
- clearTimeout(timerRef.current);
288
- }
289
- timerRef.current = setTimeout(() => {
290
- timerRef.current = null;
291
- setCopied(false);
292
- }, 2000);
288
+ if (onClick) {
289
+ onClick(event);
293
290
  }
294
291
  };
295
292
 
@@ -22,7 +22,6 @@ 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";
26
25
  import { useCopilotKit } from "../../providers/CopilotKitProvider";
27
26
  import { useCopilotChatConfiguration } from "../../providers/CopilotChatConfigurationProvider";
28
27
 
@@ -392,18 +391,14 @@ export function CopilotChatMessageView({
392
391
  // Subscribe to state changes so custom message renderers re-render when state updates.
393
392
  useEffect(() => {
394
393
  if (!config?.agentId) return;
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;
394
+ const agent = copilotkit.getAgent(config.agentId);
400
395
  if (!agent) return;
401
396
 
402
397
  const subscription = agent.subscribe({
403
398
  onStateChanged: forceUpdate,
404
399
  });
405
400
  return () => subscription.unsubscribe();
406
- }, [config?.agentId, config?.threadId, copilotkit, forceUpdate]);
401
+ }, [config?.agentId, copilotkit, forceUpdate]);
407
402
 
408
403
  // Subscribe to interrupt element changes for in-chat rendering.
409
404
  const [interruptElement, setInterruptElement] =
@@ -71,31 +71,18 @@ export function CopilotChatReasoningMessage({
71
71
  return () => clearInterval(timer);
72
72
  }, [isStreaming]);
73
73
 
74
- // Default to open while streaming, auto-collapse when streaming ends.
75
- // Track whether the user has manually toggled so auto-collapse doesn't
76
- // override their explicit intent (prevents flaky test failures on CI
77
- // where async forceUpdate timing can race with click handlers).
74
+ // Default to open while streaming, auto-collapse when streaming ends
78
75
  const [isOpen, setIsOpen] = useState(isStreaming);
79
- const userToggledRef = useRef(false);
80
76
 
81
77
  useEffect(() => {
82
78
  if (isStreaming) {
83
- // Reset user-toggle tracking when a new streaming session starts
84
- userToggledRef.current = false;
85
79
  setIsOpen(true);
86
- } else if (!userToggledRef.current) {
87
- // Auto-collapse only if the user hasn't manually toggled
80
+ } else {
81
+ // Auto-collapse when reasoning finishes
88
82
  setIsOpen(false);
89
83
  }
90
84
  }, [isStreaming]);
91
85
 
92
- const handleToggle = hasContent
93
- ? () => {
94
- userToggledRef.current = true;
95
- setIsOpen((prev) => !prev);
96
- }
97
- : undefined;
98
-
99
86
  const label = isStreaming
100
87
  ? "Thinking…"
101
88
  : `Thought for ${formatDuration(elapsed)}`;
@@ -105,7 +92,7 @@ export function CopilotChatReasoningMessage({
105
92
  label,
106
93
  hasContent,
107
94
  isStreaming,
108
- onClick: handleToggle,
95
+ onClick: hasContent ? () => setIsOpen((prev) => !prev) : undefined,
109
96
  });
110
97
 
111
98
  const boundContent = renderSlot(
@@ -18,7 +18,6 @@ import {
18
18
  type AudioInputPart,
19
19
  type VideoInputPart,
20
20
  type DocumentInputPart,
21
- copyToClipboard,
22
21
  } from "@copilotkit/shared";
23
22
  import { CopilotChatAttachmentRenderer } from "./CopilotChatAttachmentRenderer";
24
23
 
@@ -148,9 +147,12 @@ export function CopilotChatUserMessage({
148
147
  {
149
148
  onClick: async () => {
150
149
  if (flattenedContent) {
151
- return await copyToClipboard(flattenedContent);
150
+ try {
151
+ await navigator.clipboard.writeText(flattenedContent);
152
+ } catch (err) {
153
+ console.error("Failed to copy message:", err);
154
+ }
152
155
  }
153
- return false;
154
156
  },
155
157
  },
156
158
  );
@@ -312,17 +314,12 @@ export namespace CopilotChatUserMessage {
312
314
  const labels = config?.labels ?? CopilotChatDefaultLabels;
313
315
  const [copied, setCopied] = useState(false);
314
316
 
315
- const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
316
- let success = false;
317
- if (onClick) {
318
- // onClick may return a boolean indicating copy success
319
- const result = await Promise.resolve(onClick(event));
320
- success = result === true;
321
- }
317
+ const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
318
+ setCopied(true);
319
+ setTimeout(() => setCopied(false), 2000);
322
320
 
323
- if (success) {
324
- setCopied(true);
325
- setTimeout(() => setCopied(false), 2000);
321
+ if (onClick) {
322
+ onClick(event);
326
323
  }
327
324
  };
328
325