@copilotkit/react-core 1.55.0-next.9 → 1.55.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 (81) hide show
  1. package/CHANGELOG.md +36 -6
  2. package/dist/{copilotkit-DeOzjPsb.mjs → copilotkit-BY5S1-0P.mjs} +2402 -552
  3. package/dist/copilotkit-BY5S1-0P.mjs.map +1 -0
  4. package/dist/{copilotkit-BqcyhQjT.d.mts → copilotkit-BuhSUZHb.d.mts} +228 -17
  5. package/dist/copilotkit-BuhSUZHb.d.mts.map +1 -0
  6. package/dist/{copilotkit-BDNjFNmk.cjs → copilotkit-Bz5-ImDl.cjs} +2421 -541
  7. package/dist/copilotkit-Bz5-ImDl.cjs.map +1 -0
  8. package/dist/{copilotkit-l-IBF4Xp.d.cts → copilotkit-dwDWYpya.d.cts} +228 -17
  9. package/dist/copilotkit-dwDWYpya.d.cts.map +1 -0
  10. package/dist/index.cjs +1 -1
  11. package/dist/index.d.cts +1 -1
  12. package/dist/index.d.mts +1 -1
  13. package/dist/index.mjs +1 -1
  14. package/dist/index.umd.js +1400 -238
  15. package/dist/index.umd.js.map +1 -1
  16. package/dist/v2/index.cjs +13 -1
  17. package/dist/v2/index.css +1 -1
  18. package/dist/v2/index.d.cts +3 -3
  19. package/dist/v2/index.d.mts +3 -3
  20. package/dist/v2/index.mjs +3 -2
  21. package/dist/v2/index.umd.js +2442 -552
  22. package/dist/v2/index.umd.js.map +1 -1
  23. package/package.json +62 -54
  24. package/scripts/scope-preflight.mjs +1 -2
  25. package/src/components/CopilotListeners.tsx +41 -8
  26. package/src/components/copilot-provider/copilotkit-props.tsx +4 -2
  27. package/src/components/toast/toast-provider.tsx +269 -194
  28. package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +86 -22
  29. package/src/v2/__tests__/utils/test-helpers.tsx +67 -0
  30. package/src/v2/a2ui/A2UICatalogContext.tsx +79 -0
  31. package/src/v2/a2ui/A2UIMessageRenderer.tsx +125 -37
  32. package/src/v2/a2ui/A2UIToolCallRenderer.tsx +290 -0
  33. package/src/v2/components/CopilotKitInspector.tsx +2 -0
  34. package/src/v2/components/OpenGenerativeUIRenderer.tsx +598 -0
  35. package/src/v2/components/__tests__/OpenGenerativeUIRenderer.test.tsx +665 -0
  36. package/src/v2/components/chat/CopilotChat.tsx +193 -50
  37. package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +17 -2
  38. package/src/v2/components/chat/CopilotChatAttachmentQueue.tsx +481 -0
  39. package/src/v2/components/chat/CopilotChatAttachmentRenderer.tsx +139 -0
  40. package/src/v2/components/chat/CopilotChatInput.tsx +146 -77
  41. package/src/v2/components/chat/CopilotChatMessageView.tsx +253 -149
  42. package/src/v2/components/chat/CopilotChatSuggestionView.tsx +1 -0
  43. package/src/v2/components/chat/CopilotChatUserMessage.tsx +54 -0
  44. package/src/v2/components/chat/CopilotChatView.tsx +179 -66
  45. package/src/v2/components/chat/__tests__/CopilotChat.attachments.test.tsx +168 -0
  46. package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +63 -2
  47. package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +544 -1
  48. package/src/v2/components/chat/__tests__/CopilotChatPerf.e2e.test.tsx +268 -0
  49. package/src/v2/components/chat/__tests__/CopilotChatPropsRerender.e2e.test.tsx +249 -0
  50. package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +43 -2
  51. package/src/v2/components/chat/__tests__/copilot-chat-throttle.test.tsx +138 -0
  52. package/src/v2/components/chat/index.ts +9 -0
  53. package/src/v2/components/chat/scroll-element-context.ts +13 -0
  54. package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +1003 -0
  55. package/src/v2/hooks/__tests__/use-attachments.test.tsx +169 -0
  56. package/src/v2/hooks/__tests__/use-threads.test.tsx +54 -0
  57. package/src/v2/hooks/index.ts +5 -0
  58. package/src/v2/hooks/use-agent.tsx +95 -10
  59. package/src/v2/hooks/use-attachments.tsx +269 -0
  60. package/src/v2/hooks/use-frontend-tool.tsx +5 -2
  61. package/src/v2/hooks/use-render-activity-message.tsx +9 -2
  62. package/src/v2/hooks/use-threads.tsx +35 -15
  63. package/src/v2/index.ts +5 -1
  64. package/src/v2/lib/__tests__/processPartialHtml.test.ts +112 -0
  65. package/src/v2/lib/__tests__/slots.test.ts +56 -0
  66. package/src/v2/lib/processPartialHtml.ts +45 -0
  67. package/src/v2/lib/slots.tsx +42 -1
  68. package/src/v2/providers/CopilotChatConfigurationProvider.tsx +9 -3
  69. package/src/v2/providers/CopilotKitProvider.tsx +268 -32
  70. package/src/v2/providers/SandboxFunctionsContext.ts +10 -0
  71. package/src/v2/providers/__tests__/CopilotKitProvider.sandboxFunctions.test.tsx +198 -0
  72. package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +71 -0
  73. package/src/v2/providers/index.ts +7 -0
  74. package/src/v2/styles/globals.css +2 -1
  75. package/src/v2/types/index.ts +1 -0
  76. package/src/v2/types/sandbox-function.ts +11 -0
  77. package/dist/copilotkit-BDNjFNmk.cjs.map +0 -1
  78. package/dist/copilotkit-BqcyhQjT.d.mts.map +0 -1
  79. package/dist/copilotkit-DeOzjPsb.mjs.map +0 -1
  80. package/dist/copilotkit-l-IBF4Xp.d.cts.map +0 -1
  81. package/src/v2/components/__tests__/license-warning-banner.test.tsx +0 -46
@@ -3,6 +3,7 @@ import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
3
3
  import { useCopilotKit, useCopilotChatConfiguration } from "../providers";
4
4
  import { useCallback, useMemo } from "react";
5
5
  import { ReactActivityMessageRenderer } from "../types";
6
+ import { getThreadClone } from "./use-agent";
6
7
 
7
8
  export function useRenderActivityMessage() {
8
9
  const { copilotkit } = useCopilotKit();
@@ -51,7 +52,13 @@ export function useRenderActivityMessage() {
51
52
  }
52
53
 
53
54
  const Component = renderer.render;
54
- const agent = copilotkit.getAgent(agentId);
55
+ // Prefer the per-thread clone so that handleAction in ReactSurfaceHost
56
+ // calls runAgent on the same agent instance that CopilotChat renders from.
57
+ // Without this, button clicks accumulate messages on the registry agent
58
+ // while CopilotChat displays from the clone — responses appear to vanish.
59
+ const registryAgent = copilotkit.getAgent(agentId);
60
+ const agent =
61
+ getThreadClone(registryAgent, config?.threadId) ?? registryAgent;
55
62
 
56
63
  return (
57
64
  <Component
@@ -63,7 +70,7 @@ export function useRenderActivityMessage() {
63
70
  />
64
71
  );
65
72
  },
66
- [agentId, copilotkit, findRenderer],
73
+ [agentId, config?.threadId, copilotkit, findRenderer],
67
74
  );
68
75
 
69
76
  return useMemo(
@@ -6,7 +6,6 @@ import {
6
6
  ɵselectThreadsIsLoading,
7
7
  ɵselectHasNextPage,
8
8
  ɵselectIsFetchingNextPage,
9
- type ɵThread as CoreThread,
10
9
  type ɵThreadRuntimeContext,
11
10
  type ɵThreadStore,
12
11
  } from "@copilotkit/core";
@@ -24,7 +23,14 @@ import {
24
23
  * Each thread has a unique `id`, an optional human-readable `name`, and
25
24
  * timestamp fields tracking creation and update times.
26
25
  */
27
- export interface Thread extends CoreThread {}
26
+ export interface Thread {
27
+ id: string;
28
+ agentId: string;
29
+ name: string | null;
30
+ archived: boolean;
31
+ createdAt: string;
32
+ updatedAt: string;
33
+ }
28
34
 
29
35
  /**
30
36
  * Configuration for the {@link useThreads} hook.
@@ -68,18 +74,18 @@ export interface UseThreadsResult {
68
74
  error: Error | null;
69
75
  /**
70
76
  * `true` when there are more threads available to fetch via
71
- * {@link fetchNextPage}. Only meaningful when `limit` is set.
77
+ * {@link fetchMoreThreads}. Only meaningful when `limit` is set.
72
78
  */
73
- hasNextPage: boolean;
79
+ hasMoreThreads: boolean;
74
80
  /**
75
81
  * `true` while a subsequent page of threads is being fetched.
76
82
  */
77
- isFetchingNextPage: boolean;
83
+ isFetchingMoreThreads: boolean;
78
84
  /**
79
- * Fetch the next page of threads. No-op when {@link hasNextPage} is
80
- * `false` or a page fetch is already in progress.
85
+ * Fetch the next page of threads. No-op when {@link hasMoreThreads} is
86
+ * `false` or a fetch is already in progress.
81
87
  */
82
- fetchNextPage: () => void;
88
+ fetchMoreThreads: () => void;
83
89
  /**
84
90
  * Rename a thread on the platform.
85
91
  * Resolves when the server confirms the update; rejects on failure.
@@ -169,11 +175,25 @@ export function useThreads({
169
175
  }),
170
176
  );
171
177
 
172
- const threads = useThreadStoreSelector(store, ɵselectThreads);
178
+ const coreThreads = useThreadStoreSelector(store, ɵselectThreads);
179
+ const threads: Thread[] = useMemo(
180
+ () =>
181
+ coreThreads.map(
182
+ ({ id, agentId, name, archived, createdAt, updatedAt }) => ({
183
+ id,
184
+ agentId,
185
+ name,
186
+ archived,
187
+ createdAt,
188
+ updatedAt,
189
+ }),
190
+ ),
191
+ [coreThreads],
192
+ );
173
193
  const storeIsLoading = useThreadStoreSelector(store, ɵselectThreadsIsLoading);
174
194
  const storeError = useThreadStoreSelector(store, ɵselectThreadsError);
175
- const hasNextPage = useThreadStoreSelector(store, ɵselectHasNextPage);
176
- const isFetchingNextPage = useThreadStoreSelector(
195
+ const hasMoreThreads = useThreadStoreSelector(store, ɵselectHasNextPage);
196
+ const isFetchingMoreThreads = useThreadStoreSelector(
177
197
  store,
178
198
  ɵselectIsFetchingNextPage,
179
199
  );
@@ -240,15 +260,15 @@ export function useThreads({
240
260
  [store],
241
261
  );
242
262
 
243
- const fetchNextPage = useCallback(() => store.fetchNextPage(), [store]);
263
+ const fetchMoreThreads = useCallback(() => store.fetchNextPage(), [store]);
244
264
 
245
265
  return {
246
266
  threads,
247
267
  isLoading,
248
268
  error,
249
- hasNextPage,
250
- isFetchingNextPage,
251
- fetchNextPage,
269
+ hasMoreThreads,
270
+ isFetchingMoreThreads,
271
+ fetchMoreThreads,
252
272
  renameThread,
253
273
  archiveThread,
254
274
  deleteThread,
package/src/v2/index.ts CHANGED
@@ -15,8 +15,12 @@ export * from "./providers";
15
15
  export * from "./types";
16
16
  export * from "./lib/react-core";
17
17
  export { createA2UIMessageRenderer } from "./a2ui/A2UIMessageRenderer";
18
- export type { A2UIMessageRendererOptions } from "./a2ui/A2UIMessageRenderer";
18
+ export type {
19
+ A2UIMessageRendererOptions,
20
+ A2UIUserAction,
21
+ } from "./a2ui/A2UIMessageRenderer";
19
22
  export type { Theme as A2UITheme } from "@copilotkit/a2ui-renderer";
23
+ export { defaultTheme as a2uiDefaultTheme } from "@copilotkit/a2ui-renderer";
20
24
 
21
25
  // V1 backward-compat re-exports
22
26
  export { CopilotKit } from "../components/copilot-provider/copilotkit";
@@ -0,0 +1,112 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ processPartialHtml,
4
+ extractCompleteStyles,
5
+ } from "../processPartialHtml";
6
+
7
+ describe("processPartialHtml", () => {
8
+ it("returns empty string for empty input", () => {
9
+ expect(processPartialHtml("")).toBe("");
10
+ });
11
+
12
+ it("strips incomplete tag at end", () => {
13
+ expect(processPartialHtml('<div>Hello<span class="fo')).toBe("<div>Hello");
14
+ });
15
+
16
+ it("strips complete <style> blocks", () => {
17
+ const input =
18
+ "<div>Hello</div><style>.foo { color: red; }</style><p>World</p>";
19
+ expect(processPartialHtml(input)).toBe("<div>Hello</div><p>World</p>");
20
+ });
21
+
22
+ it("strips complete <script> blocks", () => {
23
+ const input = '<div>Hello</div><script>alert("hi")</script><p>World</p>';
24
+ expect(processPartialHtml(input)).toBe("<div>Hello</div><p>World</p>");
25
+ });
26
+
27
+ it("strips incomplete <style> block", () => {
28
+ const input = "<div>Hello</div><style>.foo { color:";
29
+ expect(processPartialHtml(input)).toBe("<div>Hello</div>");
30
+ });
31
+
32
+ it("strips incomplete <script> block", () => {
33
+ const input = '<div>Hello</div><script>const x = "val';
34
+ expect(processPartialHtml(input)).toBe("<div>Hello</div>");
35
+ });
36
+
37
+ it("strips incomplete HTML entities", () => {
38
+ expect(processPartialHtml("<p>Hello &amp")).toBe("<p>Hello ");
39
+ expect(processPartialHtml("<p>Hello &#123")).toBe("<p>Hello ");
40
+ });
41
+
42
+ it("preserves complete entities", () => {
43
+ expect(processPartialHtml("<p>Hello &amp; World</p>")).toBe(
44
+ "<p>Hello &amp; World</p>",
45
+ );
46
+ });
47
+
48
+ it("extracts body content from full HTML document", () => {
49
+ const input =
50
+ "<html><head><title>Test</title></head><body><p>Content</p></body></html>";
51
+ expect(processPartialHtml(input)).toBe("<p>Content</p>");
52
+ });
53
+
54
+ it("handles <body> with attributes", () => {
55
+ const input = '<body class="dark"><p>Content</p></body>';
56
+ expect(processPartialHtml(input)).toBe("<p>Content</p>");
57
+ });
58
+
59
+ it("handles no <body> tag — returns full processed string", () => {
60
+ const input = "<div><p>Just content</p></div>";
61
+ expect(processPartialHtml(input)).toBe("<div><p>Just content</p></div>");
62
+ });
63
+
64
+ it("handles combined edge cases: full document with styles, scripts, and incomplete tag", () => {
65
+ const input =
66
+ '<html><head><style>body { margin: 0; }</style></head><body><div>Hello</div><script>console.log("x")</script><p>World</p><span class="in';
67
+ expect(processPartialHtml(input)).toBe("<div>Hello</div><p>World</p>");
68
+ });
69
+
70
+ it("handles body content with incomplete style at end", () => {
71
+ const input = "<body><div>Content</div><style>.partial {";
72
+ expect(processPartialHtml(input)).toBe("<div>Content</div>");
73
+ });
74
+ });
75
+
76
+ describe("extractCompleteStyles", () => {
77
+ it("returns empty string for no styles", () => {
78
+ expect(extractCompleteStyles("<div>Hello</div>")).toBe("");
79
+ });
80
+
81
+ it("returns empty string for empty input", () => {
82
+ expect(extractCompleteStyles("")).toBe("");
83
+ });
84
+
85
+ it("extracts a single complete style block", () => {
86
+ const input =
87
+ "<div>Hello</div><style>.foo { color: red; }</style><p>World</p>";
88
+ expect(extractCompleteStyles(input)).toBe(
89
+ "<style>.foo { color: red; }</style>",
90
+ );
91
+ });
92
+
93
+ it("extracts multiple complete style blocks", () => {
94
+ const input = "<style>a{}</style><div>X</div><style>b{}</style>";
95
+ expect(extractCompleteStyles(input)).toBe(
96
+ "<style>a{}</style><style>b{}</style>",
97
+ );
98
+ });
99
+
100
+ it("ignores incomplete style blocks", () => {
101
+ const input = "<style>.complete{}</style><style>.incomplete {";
102
+ expect(extractCompleteStyles(input)).toBe("<style>.complete{}</style>");
103
+ });
104
+
105
+ it("extracts styles from head", () => {
106
+ const input =
107
+ "<head><style>body { margin: 0; }</style></head><body><p>Hi</p></body>";
108
+ expect(extractCompleteStyles(input)).toBe(
109
+ "<style>body { margin: 0; }</style>",
110
+ );
111
+ });
112
+ });
@@ -0,0 +1,56 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { describe, it, expect } from "vitest";
3
+ import { useShallowStableRef } from "../slots";
4
+
5
+ describe("useShallowStableRef", () => {
6
+ it("returns the same reference when called twice with shallowly equal plain objects", () => {
7
+ const initial = { a: 1 };
8
+ const { result, rerender } = renderHook(
9
+ ({ value }: { value: { a: number } }) => useShallowStableRef(value),
10
+ { initialProps: { value: initial } },
11
+ );
12
+
13
+ const firstRef = result.current;
14
+ rerender({ value: { a: 1 } }); // new object, same shape
15
+ expect(result.current).toBe(firstRef);
16
+ });
17
+
18
+ it("updates the reference when the value changes", () => {
19
+ const { result, rerender } = renderHook(
20
+ ({ value }: { value: { a: number } }) => useShallowStableRef(value),
21
+ { initialProps: { value: { a: 1 } } },
22
+ );
23
+
24
+ const firstRef = result.current;
25
+ rerender({ value: { a: 2 } });
26
+ expect(result.current).not.toBe(firstRef);
27
+ expect(result.current).toEqual({ a: 2 });
28
+ });
29
+
30
+ it("handles undefined without crashing", () => {
31
+ const { result } = renderHook(() =>
32
+ useShallowStableRef(undefined as unknown as { a: number }),
33
+ );
34
+ expect(result.current).toBeUndefined();
35
+ });
36
+
37
+ it("handles null without crashing", () => {
38
+ const { result } = renderHook(() =>
39
+ useShallowStableRef(null as unknown as { a: number }),
40
+ );
41
+ expect(result.current).toBeNull();
42
+ });
43
+
44
+ it("does not shallow-compare arrays — treats them by reference", () => {
45
+ const arr1 = [1, 2, 3];
46
+ const { result, rerender } = renderHook(
47
+ ({ value }: { value: number[] }) => useShallowStableRef(value),
48
+ { initialProps: { value: arr1 } },
49
+ );
50
+
51
+ const firstRef = result.current;
52
+ rerender({ value: [1, 2, 3] }); // new array, same contents
53
+ // arrays are not plain objects — reference should update
54
+ expect(result.current).not.toBe(firstRef);
55
+ });
56
+ });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Extracts all complete `<style>` blocks from the raw HTML.
3
+ * Returns the concatenated style tags, suitable for injection into `<head>`.
4
+ */
5
+ export function extractCompleteStyles(html: string): string {
6
+ const matches = html.match(/<style\b[^>]*>[\s\S]*?<\/style>/gi);
7
+ return matches ? matches.join("") : "";
8
+ }
9
+
10
+ /**
11
+ * Processes raw accumulated HTML for safe preview via innerHTML injection.
12
+ * Pure function, no DOM dependencies.
13
+ *
14
+ * Pipeline (order matters):
15
+ * 1. Strip incomplete tag at end
16
+ * 2. Strip complete <style>, <script>, and <head> blocks
17
+ * 3. Strip incomplete <style>/<script>/<head> blocks
18
+ * 4. Strip incomplete HTML entities
19
+ * 5. Extract body content (or use full string if no <body>)
20
+ */
21
+ export function processPartialHtml(html: string): string {
22
+ let result = html;
23
+
24
+ // 1. Strip incomplete tag at end — e.g. `<div class="fo`
25
+ result = result.replace(/<[^>]*$/, "");
26
+
27
+ // 2. Strip complete <style>, <script>, and <head> blocks
28
+ result = result.replace(/<(style|script|head)\b[^>]*>[\s\S]*?<\/\1>/gi, "");
29
+
30
+ // 3. Strip incomplete <style>/<script>/<head> blocks (opening tag, no close)
31
+ result = result.replace(/<(style|script|head)\b[^>]*>[\s\S]*$/gi, "");
32
+
33
+ // 4. Strip incomplete HTML entities — e.g. `&amp` without semicolon
34
+ result = result.replace(/&[a-zA-Z0-9#]*$/, "");
35
+
36
+ // 5. Extract body content
37
+ const bodyMatch = result.match(/<body[^>]*>([\s\S]*)/i);
38
+ if (bodyMatch) {
39
+ result = bodyMatch[1]!;
40
+ // Strip </body> and everything after
41
+ result = result.replace(/<\/body>[\s\S]*/i, "");
42
+ }
43
+
44
+ return result;
45
+ }
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import React, { useRef } from "react";
2
2
  import { twMerge } from "tailwind-merge";
3
3
 
4
4
  /** Existing union (unchanged) */
@@ -26,6 +26,47 @@ export function shallowEqual<T extends Record<string, unknown>>(
26
26
  return true;
27
27
  }
28
28
 
29
+ /**
30
+ * Returns true only for plain JS objects (`{}`), excluding arrays, Dates,
31
+ * class instances, and other exotic objects that happen to have typeof "object".
32
+ */
33
+ function isPlainObject(obj: unknown): obj is Record<string, unknown> {
34
+ return (
35
+ obj !== null &&
36
+ typeof obj === "object" &&
37
+ Object.prototype.toString.call(obj) === "[object Object]"
38
+ );
39
+ }
40
+
41
+ /**
42
+ * Returns the same reference as long as the value is shallowly equal to the
43
+ * previous render's value.
44
+ *
45
+ * - Identical references bail out immediately (O(1)).
46
+ * - Plain objects ({}) are shallow-compared key-by-key.
47
+ * - Arrays, Dates, class instances, functions, and primitives are compared by
48
+ * reference only — shallowEqual is never called on non-plain objects, which
49
+ * avoids incorrect equality for e.g. [1,2] vs [1,2] (different arrays).
50
+ *
51
+ * Typical use: stabilize inline slot props so MemoizedSlotWrapper's shallow
52
+ * equality check isn't defeated by a new object reference on every render.
53
+ */
54
+ export function useShallowStableRef<T>(value: T): T {
55
+ const ref = useRef(value);
56
+
57
+ // 1. Identical reference — bail early, no comparison needed.
58
+ if (ref.current === value) return ref.current;
59
+
60
+ // 2. Both are plain objects — shallow-compare to detect structural equality.
61
+ if (isPlainObject(ref.current) && isPlainObject(value)) {
62
+ if (shallowEqual(ref.current, value)) return ref.current;
63
+ }
64
+
65
+ // 3. Different values (or non-comparable types) — update the ref.
66
+ ref.current = value;
67
+ return ref.current;
68
+ }
69
+
29
70
  /** Utility: concrete React elements for every slot */
30
71
  type SlotElements<S> = { [K in keyof S]: React.ReactElement };
31
72
 
@@ -9,6 +9,7 @@ import React, {
9
9
  useState,
10
10
  } from "react";
11
11
  import { DEFAULT_AGENT_ID, randomUUID } from "@copilotkit/shared";
12
+ import { useShallowStableRef } from "../lib/slots";
12
13
 
13
14
  // Default labels
14
15
  export const CopilotChatDefaultLabels = {
@@ -16,7 +17,7 @@ export const CopilotChatDefaultLabels = {
16
17
  chatInputToolbarStartTranscribeButtonLabel: "Transcribe",
17
18
  chatInputToolbarCancelTranscribeButtonLabel: "Cancel",
18
19
  chatInputToolbarFinishTranscribeButtonLabel: "Finish",
19
- chatInputToolbarAddButtonLabel: "Add photos or files",
20
+ chatInputToolbarAddButtonLabel: "Add attachments",
20
21
  chatInputToolbarToolsButtonLabel: "Tools",
21
22
  assistantMessageToolbarCopyCodeLabel: "Copy",
22
23
  assistantMessageToolbarCopyCodeCopiedLabel: "Copied",
@@ -65,13 +66,18 @@ export const CopilotChatConfigurationProvider: React.FC<
65
66
  > = ({ children, labels, agentId, threadId, isModalDefaultOpen }) => {
66
67
  const parentConfig = useContext(CopilotChatConfiguration);
67
68
 
69
+ // Stabilize labels references so that inline objects (new reference on every
70
+ // parent render) don't invalidate mergedLabels and churn the context value.
71
+ // parentConfig?.labels is already stabilized by the parent provider's own
72
+ // useShallowStableRef, so we only need to stabilize the local labels prop.
73
+ const stableLabels = useShallowStableRef(labels);
68
74
  const mergedLabels: CopilotChatLabels = useMemo(
69
75
  () => ({
70
76
  ...CopilotChatDefaultLabels,
71
77
  ...(parentConfig?.labels ?? {}),
72
- ...(labels ?? {}),
78
+ ...(stableLabels ?? {}),
73
79
  }),
74
- [labels, parentConfig?.labels],
80
+ [stableLabels, parentConfig?.labels],
75
81
  );
76
82
 
77
83
  const resolvedAgentId = agentId ?? parentConfig?.agentId ?? DEFAULT_AGENT_ID;