@copilotkit/react-core 1.54.1 → 1.55.0-next.7

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 (183) hide show
  1. package/CHANGELOG.md +117 -116
  2. package/dist/copilotkit-B3Mb1yVE.cjs +7975 -0
  3. package/dist/copilotkit-B3Mb1yVE.cjs.map +1 -0
  4. package/dist/copilotkit-DBzgOMby.d.cts +2182 -0
  5. package/dist/copilotkit-DBzgOMby.d.cts.map +1 -0
  6. package/dist/copilotkit-DNYSFuz5.mjs +7562 -0
  7. package/dist/copilotkit-DNYSFuz5.mjs.map +1 -0
  8. package/dist/copilotkit-Dy5w3qEV.d.mts +2182 -0
  9. package/dist/copilotkit-Dy5w3qEV.d.mts.map +1 -0
  10. package/dist/index.cjs +27 -28
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +3 -3
  13. package/dist/index.d.cts.map +1 -1
  14. package/dist/index.d.mts +3 -3
  15. package/dist/index.d.mts.map +1 -1
  16. package/dist/index.mjs +4 -5
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/index.umd.js +1941 -35
  19. package/dist/index.umd.js.map +1 -1
  20. package/dist/v2/index.cjs +77 -7
  21. package/dist/v2/index.css +1 -2
  22. package/dist/v2/index.d.cts +6 -4
  23. package/dist/v2/index.d.mts +6 -4
  24. package/dist/v2/index.mjs +7 -4
  25. package/dist/v2/index.umd.js +5725 -24
  26. package/dist/v2/index.umd.js.map +1 -1
  27. package/package.json +37 -9
  28. package/scripts/scope-preflight.mjs +101 -0
  29. package/src/components/CopilotListeners.tsx +2 -6
  30. package/src/components/copilot-provider/copilot-messages.tsx +1 -1
  31. package/src/components/copilot-provider/copilotkit-props.tsx +1 -1
  32. package/src/components/copilot-provider/copilotkit.tsx +4 -4
  33. package/src/context/copilot-messages-context.tsx +1 -1
  34. package/src/hooks/__tests__/use-coagent-config.test.ts +2 -2
  35. package/src/hooks/__tests__/use-coagent-state-render.e2e.test.tsx +2 -2
  36. package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +3 -7
  37. package/src/hooks/__tests__/use-frontend-tool-available.test.tsx +1 -1
  38. package/src/hooks/__tests__/use-frontend-tool-remount.e2e.test.tsx +4 -4
  39. package/src/hooks/use-agent-nodename.ts +1 -1
  40. package/src/hooks/use-coagent-state-render-bridge.tsx +1 -4
  41. package/src/hooks/use-coagent.ts +1 -1
  42. package/src/hooks/use-configure-chat-suggestions.tsx +2 -2
  43. package/src/hooks/use-copilot-chat-suggestions.tsx +2 -2
  44. package/src/hooks/use-copilot-chat_internal.ts +2 -2
  45. package/src/hooks/use-copilot-readable.ts +1 -1
  46. package/src/hooks/use-frontend-tool.ts +2 -2
  47. package/src/hooks/use-human-in-the-loop.ts +2 -2
  48. package/src/hooks/use-langgraph-interrupt.ts +2 -5
  49. package/src/hooks/use-lazy-tool-renderer.tsx +1 -1
  50. package/src/hooks/use-render-tool-call.ts +1 -1
  51. package/src/lib/copilot-task.ts +1 -1
  52. package/src/setupTests.ts +18 -14
  53. package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +176 -0
  54. package/src/v2/__tests__/globalSetup.ts +14 -0
  55. package/src/v2/__tests__/setup.ts +93 -0
  56. package/src/v2/__tests__/utils/test-helpers.tsx +470 -0
  57. package/src/v2/a2ui/A2UIMessageRenderer.tsx +206 -0
  58. package/src/v2/components/CopilotKitInspector.tsx +50 -0
  59. package/src/v2/components/MCPAppsActivityRenderer.tsx +785 -0
  60. package/src/v2/components/WildcardToolCallRender.tsx +86 -0
  61. package/src/v2/components/__tests__/license-warning-banner.test.tsx +46 -0
  62. package/src/v2/components/chat/CopilotChat.tsx +431 -0
  63. package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +375 -0
  64. package/src/v2/components/chat/CopilotChatAudioRecorder.tsx +350 -0
  65. package/src/v2/components/chat/CopilotChatInput.tsx +1302 -0
  66. package/src/v2/components/chat/CopilotChatMessageView.tsx +556 -0
  67. package/src/v2/components/chat/CopilotChatReasoningMessage.tsx +252 -0
  68. package/src/v2/components/chat/CopilotChatSuggestionPill.tsx +59 -0
  69. package/src/v2/components/chat/CopilotChatSuggestionView.tsx +133 -0
  70. package/src/v2/components/chat/CopilotChatToggleButton.tsx +171 -0
  71. package/src/v2/components/chat/CopilotChatToolCallsView.tsx +40 -0
  72. package/src/v2/components/chat/CopilotChatUserMessage.tsx +388 -0
  73. package/src/v2/components/chat/CopilotChatView.tsx +598 -0
  74. package/src/v2/components/chat/CopilotModalHeader.tsx +129 -0
  75. package/src/v2/components/chat/CopilotPopup.tsx +81 -0
  76. package/src/v2/components/chat/CopilotPopupView.tsx +317 -0
  77. package/src/v2/components/chat/CopilotSidebar.tsx +76 -0
  78. package/src/v2/components/chat/CopilotSidebarView.tsx +255 -0
  79. package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.tsx +1113 -0
  80. package/src/v2/components/chat/__tests__/CopilotChat.onError.test.tsx +73 -0
  81. package/src/v2/components/chat/__tests__/CopilotChat.slots.e2e.test.tsx +432 -0
  82. package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +150 -0
  83. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.slots.e2e.test.tsx +624 -0
  84. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.tsx +702 -0
  85. package/src/v2/components/chat/__tests__/CopilotChatCssClasses.test.tsx +107 -0
  86. package/src/v2/components/chat/__tests__/CopilotChatInput.slots.e2e.test.tsx +929 -0
  87. package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +986 -0
  88. package/src/v2/components/chat/__tests__/CopilotChatMessageView.slots.e2e.test.tsx +1004 -0
  89. package/src/v2/components/chat/__tests__/CopilotChatMessageView.test.tsx +169 -0
  90. package/src/v2/components/chat/__tests__/CopilotChatSuggestionView.slots.e2e.test.tsx +530 -0
  91. package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.tsx +782 -0
  92. package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +2413 -0
  93. package/src/v2/components/chat/__tests__/CopilotChatUserMessage.slots.e2e.test.tsx +621 -0
  94. package/src/v2/components/chat/__tests__/CopilotChatView.onClick.e2e.test.tsx +853 -0
  95. package/src/v2/components/chat/__tests__/CopilotChatView.slots.e2e.test.tsx +1050 -0
  96. package/src/v2/components/chat/__tests__/CopilotModalHeader.slots.e2e.test.tsx +484 -0
  97. package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.tsx +612 -0
  98. package/src/v2/components/chat/__tests__/CopilotSidebarView.slots.e2e.test.tsx +502 -0
  99. package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +1011 -0
  100. package/src/v2/components/chat/__tests__/setup.ts +1 -0
  101. package/src/v2/components/chat/index.ts +79 -0
  102. package/src/v2/components/index.ts +7 -0
  103. package/src/v2/components/license-warning-banner.tsx +198 -0
  104. package/src/v2/components/ui/button.tsx +123 -0
  105. package/src/v2/components/ui/dropdown-menu.tsx +258 -0
  106. package/src/v2/components/ui/tooltip.tsx +60 -0
  107. package/src/v2/hooks/__tests__/standard-schema-types.test.tsx +152 -0
  108. package/src/v2/hooks/__tests__/standard-schema.test.tsx +282 -0
  109. package/src/v2/hooks/__tests__/use-agent-context-timing.e2e.test.tsx +132 -0
  110. package/src/v2/hooks/__tests__/use-agent-context.test.tsx +401 -0
  111. package/src/v2/hooks/__tests__/use-agent-error-state.test.tsx +44 -0
  112. package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +205 -0
  113. package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +148 -0
  114. package/src/v2/hooks/__tests__/use-component.test.tsx +123 -0
  115. package/src/v2/hooks/__tests__/use-configure-suggestions.e2e.test.tsx +696 -0
  116. package/src/v2/hooks/__tests__/use-default-render-tool.test.tsx +153 -0
  117. package/src/v2/hooks/__tests__/use-frontend-tool-available.test.tsx +167 -0
  118. package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +2129 -0
  119. package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.tsx +1261 -0
  120. package/src/v2/hooks/__tests__/use-interrupt.test.tsx +397 -0
  121. package/src/v2/hooks/__tests__/use-katex-styles.test.tsx +56 -0
  122. package/src/v2/hooks/__tests__/use-keyboard-height.test.tsx +192 -0
  123. package/src/v2/hooks/__tests__/use-render-tool.test.tsx +259 -0
  124. package/src/v2/hooks/__tests__/use-suggestions.e2e.test.tsx +524 -0
  125. package/src/v2/hooks/__tests__/use-threads.test.tsx +433 -0
  126. package/src/v2/hooks/__tests__/zod-regression.test.tsx +311 -0
  127. package/src/v2/hooks/index.ts +18 -0
  128. package/src/v2/hooks/use-agent-context.tsx +45 -0
  129. package/src/v2/hooks/use-agent.tsx +155 -0
  130. package/src/v2/hooks/use-component.tsx +89 -0
  131. package/src/v2/hooks/use-configure-suggestions.tsx +187 -0
  132. package/src/v2/hooks/use-default-render-tool.tsx +254 -0
  133. package/src/v2/hooks/use-frontend-tool.tsx +43 -0
  134. package/src/v2/hooks/use-human-in-the-loop.tsx +81 -0
  135. package/src/v2/hooks/use-interrupt.tsx +305 -0
  136. package/src/v2/hooks/use-keyboard-height.tsx +67 -0
  137. package/src/v2/hooks/use-render-activity-message.tsx +73 -0
  138. package/src/v2/hooks/use-render-custom-messages.tsx +93 -0
  139. package/src/v2/hooks/use-render-tool-call.tsx +175 -0
  140. package/src/v2/hooks/use-render-tool.tsx +181 -0
  141. package/src/v2/hooks/use-suggestions.tsx +91 -0
  142. package/src/v2/hooks/use-threads.tsx +256 -0
  143. package/src/v2/hooks/useKatexStyles.ts +27 -0
  144. package/src/v2/index.css +1 -1
  145. package/src/v2/index.ts +18 -2
  146. package/src/v2/lib/__tests__/completePartialMarkdown.test.ts +495 -0
  147. package/src/v2/lib/__tests__/renderSlot.test.tsx +588 -0
  148. package/src/v2/lib/react-core.ts +156 -0
  149. package/src/v2/lib/slots.tsx +143 -0
  150. package/src/v2/lib/transcription-client.ts +184 -0
  151. package/src/v2/lib/utils.ts +8 -0
  152. package/src/v2/providers/CopilotChatConfigurationProvider.tsx +162 -0
  153. package/src/v2/providers/CopilotKitProvider.tsx +600 -0
  154. package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.tsx +546 -0
  155. package/src/v2/providers/__tests__/CopilotKitProvider.license.test.tsx +101 -0
  156. package/src/v2/providers/__tests__/CopilotKitProvider.onError.test.tsx +69 -0
  157. package/src/v2/providers/__tests__/CopilotKitProvider.renderCustomMessages.e2e.test.tsx +881 -0
  158. package/src/v2/providers/__tests__/CopilotKitProvider.stability.test.tsx +740 -0
  159. package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +642 -0
  160. package/src/v2/providers/__tests__/CopilotKitProvider.wildcard.test.tsx +294 -0
  161. package/src/v2/providers/index.ts +14 -0
  162. package/src/v2/styles/globals.css +230 -0
  163. package/src/v2/types/__tests__/defineToolCallRenderer.test.tsx +525 -0
  164. package/src/v2/types/defineToolCallRenderer.ts +65 -0
  165. package/src/v2/types/frontend-tool.ts +8 -0
  166. package/src/v2/types/human-in-the-loop.ts +33 -0
  167. package/src/v2/types/index.ts +7 -0
  168. package/src/v2/types/interrupt.ts +15 -0
  169. package/src/v2/types/react-activity-message-renderer.ts +27 -0
  170. package/src/v2/types/react-custom-message-renderer.ts +17 -0
  171. package/src/v2/types/react-tool-call-renderer.ts +32 -0
  172. package/tsdown.config.ts +34 -10
  173. package/vitest.config.mjs +4 -3
  174. package/LICENSE +0 -21
  175. package/dist/copilotkit-BRPQ2sqS.d.cts +0 -670
  176. package/dist/copilotkit-BRPQ2sqS.d.cts.map +0 -1
  177. package/dist/copilotkit-C94ayZbs.cjs +0 -2161
  178. package/dist/copilotkit-C94ayZbs.cjs.map +0 -1
  179. package/dist/copilotkit-CwZMFmSK.d.mts +0 -670
  180. package/dist/copilotkit-CwZMFmSK.d.mts.map +0 -1
  181. package/dist/copilotkit-Yh_Ld_FX.mjs +0 -2031
  182. package/dist/copilotkit-Yh_Ld_FX.mjs.map +0 -1
  183. package/dist/v2/index.css.map +0 -1
@@ -0,0 +1,546 @@
1
+ import React from "react";
2
+ import { render, screen, fireEvent, act } from "@testing-library/react";
3
+ import { describe, it, expect } from "vitest";
4
+ import {
5
+ CopilotChatConfigurationProvider,
6
+ CopilotChatDefaultLabels,
7
+ useCopilotChatConfiguration,
8
+ } from "../CopilotChatConfigurationProvider";
9
+ import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
10
+ import { CopilotKitProvider } from "../CopilotKitProvider";
11
+ import { MockStepwiseAgent } from "../../__tests__/utils/test-helpers";
12
+ import { CopilotChat } from "../../components/chat/CopilotChat";
13
+
14
+ // Test component to access configuration
15
+ function ConfigurationDisplay() {
16
+ const config = useCopilotChatConfiguration();
17
+ return (
18
+ <div>
19
+ <div data-testid="agentId">{config?.agentId || "no-config"}</div>
20
+ <div data-testid="threadId">{config?.threadId || "no-config"}</div>
21
+ <div data-testid="placeholder">
22
+ {config?.labels.chatInputPlaceholder || "no-config"}
23
+ </div>
24
+ <div data-testid="copyLabel">
25
+ {config?.labels.assistantMessageToolbarCopyMessageLabel || "no-config"}
26
+ </div>
27
+ </div>
28
+ );
29
+ }
30
+
31
+ describe("CopilotChatConfigurationProvider", () => {
32
+ describe("Basic functionality", () => {
33
+ it("should provide default configuration", () => {
34
+ render(
35
+ <CopilotChatConfigurationProvider threadId="test-thread">
36
+ <ConfigurationDisplay />
37
+ </CopilotChatConfigurationProvider>,
38
+ );
39
+
40
+ expect(screen.getByTestId("agentId").textContent).toBe(DEFAULT_AGENT_ID);
41
+ expect(screen.getByTestId("threadId").textContent).toBe("test-thread");
42
+ expect(screen.getByTestId("placeholder").textContent).toBe(
43
+ CopilotChatDefaultLabels.chatInputPlaceholder,
44
+ );
45
+ });
46
+
47
+ it("should accept custom agentId", () => {
48
+ render(
49
+ <CopilotChatConfigurationProvider
50
+ threadId="test-thread"
51
+ agentId="custom-agent"
52
+ >
53
+ <ConfigurationDisplay />
54
+ </CopilotChatConfigurationProvider>,
55
+ );
56
+
57
+ expect(screen.getByTestId("agentId").textContent).toBe("custom-agent");
58
+ });
59
+
60
+ it("should merge custom labels with defaults", () => {
61
+ const customLabels = {
62
+ chatInputPlaceholder: "Custom placeholder",
63
+ };
64
+
65
+ render(
66
+ <CopilotChatConfigurationProvider
67
+ threadId="test-thread"
68
+ labels={customLabels}
69
+ >
70
+ <ConfigurationDisplay />
71
+ </CopilotChatConfigurationProvider>,
72
+ );
73
+
74
+ expect(screen.getByTestId("placeholder").textContent).toBe(
75
+ "Custom placeholder",
76
+ );
77
+ // Other labels should still have defaults
78
+ expect(screen.getByTestId("copyLabel").textContent).toBe(
79
+ CopilotChatDefaultLabels.assistantMessageToolbarCopyMessageLabel,
80
+ );
81
+ });
82
+ });
83
+
84
+ describe("Hook behavior", () => {
85
+ it("should return null when no provider exists", () => {
86
+ render(<ConfigurationDisplay />);
87
+
88
+ expect(screen.getByTestId("agentId").textContent).toBe("no-config");
89
+ expect(screen.getByTestId("threadId").textContent).toBe("no-config");
90
+ expect(screen.getByTestId("placeholder").textContent).toBe("no-config");
91
+ });
92
+ });
93
+
94
+ describe("CopilotChat priority merging", () => {
95
+ it("should use defaults when no provider exists and no props passed", () => {
96
+ // CopilotChat creates its own provider, so we need to check inside it
97
+ // We'll check the input placeholder which uses the configuration
98
+ const { container } = render(
99
+ <CopilotKitProvider
100
+ agents__unsafe_dev_only={{
101
+ [DEFAULT_AGENT_ID]: new MockStepwiseAgent(),
102
+ }}
103
+ >
104
+ <CopilotChat />
105
+ </CopilotKitProvider>,
106
+ );
107
+
108
+ // Find the input element and check its placeholder
109
+ const input = container.querySelector('textarea, input[type="text"]');
110
+ expect(input?.getAttribute("placeholder")).toBe(
111
+ CopilotChatDefaultLabels.chatInputPlaceholder,
112
+ );
113
+ });
114
+
115
+ it("should inherit from existing provider when CopilotChat has no props", () => {
116
+ const { container } = render(
117
+ <CopilotKitProvider
118
+ agents__unsafe_dev_only={{
119
+ "outer-agent": new MockStepwiseAgent({ agentId: "outer-agent" }),
120
+ }}
121
+ >
122
+ <CopilotChatConfigurationProvider
123
+ threadId="outer-thread"
124
+ agentId="outer-agent"
125
+ labels={{ chatInputPlaceholder: "Outer placeholder" }}
126
+ >
127
+ <CopilotChat />
128
+ </CopilotChatConfigurationProvider>
129
+ </CopilotKitProvider>,
130
+ );
131
+
132
+ // Check that the input inherits the outer placeholder
133
+ const input = container.querySelector('textarea, input[type="text"]');
134
+ expect(input?.getAttribute("placeholder")).toBe("Outer placeholder");
135
+ });
136
+
137
+ it("should override existing provider with CopilotChat props", () => {
138
+ const { container } = render(
139
+ <CopilotKitProvider
140
+ agents__unsafe_dev_only={{
141
+ "inner-agent": new MockStepwiseAgent({ agentId: "inner-agent" }),
142
+ }}
143
+ >
144
+ <CopilotChatConfigurationProvider
145
+ threadId="outer-thread"
146
+ agentId="outer-agent"
147
+ labels={{ chatInputPlaceholder: "Outer placeholder" }}
148
+ >
149
+ <CopilotChat
150
+ agentId="inner-agent"
151
+ threadId="inner-thread"
152
+ labels={{ chatInputPlaceholder: "Inner placeholder" }}
153
+ />
154
+ </CopilotChatConfigurationProvider>
155
+ </CopilotKitProvider>,
156
+ );
157
+
158
+ // CopilotChat props should win - check the input placeholder
159
+ const input = container.querySelector('textarea, input[type="text"]');
160
+ expect(input?.getAttribute("placeholder")).toBe("Inner placeholder");
161
+ });
162
+
163
+ it("should merge labels correctly with priority: default < existing < props", () => {
164
+ const { container } = render(
165
+ <CopilotKitProvider
166
+ agents__unsafe_dev_only={{
167
+ [DEFAULT_AGENT_ID]: new MockStepwiseAgent(),
168
+ }}
169
+ >
170
+ <CopilotChatConfigurationProvider
171
+ threadId="outer-thread"
172
+ labels={{
173
+ chatInputPlaceholder: "Outer placeholder",
174
+ assistantMessageToolbarCopyMessageLabel: "Outer copy",
175
+ }}
176
+ >
177
+ <CopilotChat
178
+ labels={{
179
+ chatInputPlaceholder: "Inner placeholder",
180
+ // Not overriding copyLabel, should inherit from outer
181
+ }}
182
+ />
183
+ </CopilotChatConfigurationProvider>
184
+ </CopilotKitProvider>,
185
+ );
186
+
187
+ const input = container.querySelector('textarea, input[type="text"]');
188
+ expect(input?.getAttribute("placeholder")).toBe("Inner placeholder");
189
+ // The copy label would be tested if we had assistant messages
190
+ });
191
+
192
+ it("should handle partial overrides correctly", () => {
193
+ const { container } = render(
194
+ <CopilotKitProvider
195
+ agents__unsafe_dev_only={{
196
+ "outer-agent": new MockStepwiseAgent({ agentId: "outer-agent" }),
197
+ }}
198
+ >
199
+ <CopilotChatConfigurationProvider
200
+ threadId="outer-thread"
201
+ agentId="outer-agent"
202
+ labels={{ chatInputPlaceholder: "Outer placeholder" }}
203
+ >
204
+ <CopilotChat
205
+ // Only override threadId and some labels, not agentId
206
+ threadId="inner-thread"
207
+ labels={{
208
+ chatInputPlaceholder: "Inner placeholder",
209
+ }}
210
+ />
211
+ </CopilotChatConfigurationProvider>
212
+ </CopilotKitProvider>,
213
+ );
214
+
215
+ // Check the placeholder was overridden
216
+ const input = container.querySelector('textarea, input[type="text"]');
217
+ expect(input?.getAttribute("placeholder")).toBe("Inner placeholder");
218
+ // agentId and other properties would be tested through agent behavior
219
+ });
220
+
221
+ it("should allow accessing configuration outside CopilotChat in same provider", () => {
222
+ // This shows that ConfigurationDisplay outside CopilotChat
223
+ // sees the outer provider values, not the inner merged ones
224
+ render(
225
+ <CopilotKitProvider
226
+ agents__unsafe_dev_only={{
227
+ "outer-agent": new MockStepwiseAgent({ agentId: "outer-agent" }),
228
+ }}
229
+ >
230
+ <CopilotChatConfigurationProvider
231
+ threadId="outer-thread"
232
+ agentId="outer-agent"
233
+ labels={{ chatInputPlaceholder: "Outer placeholder" }}
234
+ >
235
+ <CopilotChat
236
+ threadId="inner-thread"
237
+ labels={{ chatInputPlaceholder: "Inner placeholder" }}
238
+ />
239
+ <ConfigurationDisplay />
240
+ </CopilotChatConfigurationProvider>
241
+ </CopilotKitProvider>,
242
+ );
243
+
244
+ // ConfigurationDisplay is outside CopilotChat, so it sees outer values
245
+ expect(screen.getByTestId("agentId").textContent).toBe("outer-agent");
246
+ expect(screen.getByTestId("threadId").textContent).toBe("outer-thread");
247
+ expect(screen.getByTestId("placeholder").textContent).toBe(
248
+ "Outer placeholder",
249
+ );
250
+ });
251
+ });
252
+
253
+ describe("Modal state", () => {
254
+ it("should always provide setModalOpen and isModalOpen even without isModalDefaultOpen", () => {
255
+ function ModalStateDisplay() {
256
+ const config = useCopilotChatConfiguration();
257
+ return (
258
+ <div>
259
+ <div data-testid="hasSetModalOpen">
260
+ {config?.setModalOpen ? "yes" : "no"}
261
+ </div>
262
+ <div data-testid="hasIsModalOpen">
263
+ {config?.isModalOpen !== undefined ? "yes" : "no"}
264
+ </div>
265
+ </div>
266
+ );
267
+ }
268
+
269
+ render(
270
+ <CopilotChatConfigurationProvider threadId="test-thread">
271
+ <ModalStateDisplay />
272
+ </CopilotChatConfigurationProvider>,
273
+ );
274
+
275
+ expect(screen.getByTestId("hasSetModalOpen").textContent).toBe("yes");
276
+ expect(screen.getByTestId("hasIsModalOpen").textContent).toBe("yes");
277
+ });
278
+
279
+ it("should respect isModalDefaultOpen when provided", () => {
280
+ function ModalStateDisplay() {
281
+ const config = useCopilotChatConfiguration();
282
+ return (
283
+ <div>
284
+ <div data-testid="isModalOpen">
285
+ {config?.isModalOpen ? "open" : "closed"}
286
+ </div>
287
+ </div>
288
+ );
289
+ }
290
+
291
+ render(
292
+ <CopilotChatConfigurationProvider
293
+ threadId="test-thread"
294
+ isModalDefaultOpen={false}
295
+ >
296
+ <ModalStateDisplay />
297
+ </CopilotChatConfigurationProvider>,
298
+ );
299
+
300
+ expect(screen.getByTestId("isModalOpen").textContent).toBe("closed");
301
+ });
302
+
303
+ it("should inherit parent modal state when child has no isModalDefaultOpen", () => {
304
+ function ModalStateDisplay() {
305
+ const config = useCopilotChatConfiguration();
306
+ return (
307
+ <div>
308
+ <div data-testid="isModalOpen">
309
+ {config?.isModalOpen ? "open" : "closed"}
310
+ </div>
311
+ <div data-testid="hasSetModalOpen">
312
+ {config?.setModalOpen ? "yes" : "no"}
313
+ </div>
314
+ </div>
315
+ );
316
+ }
317
+
318
+ render(
319
+ <CopilotChatConfigurationProvider
320
+ threadId="outer-thread"
321
+ isModalDefaultOpen={false}
322
+ >
323
+ <CopilotChatConfigurationProvider threadId="inner-thread">
324
+ <ModalStateDisplay />
325
+ </CopilotChatConfigurationProvider>
326
+ </CopilotChatConfigurationProvider>,
327
+ );
328
+
329
+ // Child should inherit parent's modal state (closed)
330
+ expect(screen.getByTestId("isModalOpen").textContent).toBe("closed");
331
+ expect(screen.getByTestId("hasSetModalOpen").textContent).toBe("yes");
332
+ });
333
+
334
+ it("should allow nested provider to override parent modal state with explicit isModalDefaultOpen", () => {
335
+ function ModalStateDisplay() {
336
+ const config = useCopilotChatConfiguration();
337
+ return (
338
+ <div>
339
+ <div data-testid="isModalOpen">
340
+ {config?.isModalOpen ? "open" : "closed"}
341
+ </div>
342
+ </div>
343
+ );
344
+ }
345
+
346
+ render(
347
+ <CopilotChatConfigurationProvider
348
+ threadId="outer-thread"
349
+ isModalDefaultOpen={true}
350
+ >
351
+ <CopilotChatConfigurationProvider
352
+ threadId="inner-thread"
353
+ isModalDefaultOpen={false}
354
+ >
355
+ <ModalStateDisplay />
356
+ </CopilotChatConfigurationProvider>
357
+ </CopilotChatConfigurationProvider>,
358
+ );
359
+
360
+ expect(screen.getByTestId("isModalOpen").textContent).toBe("closed");
361
+ });
362
+ });
363
+
364
+ /**
365
+ * CPK-7152: Bidirectional sync between nested providers.
366
+ *
367
+ * The fix must satisfy both:
368
+ * Behavior A — child provider respects its own isModalDefaultOpen even
369
+ * when a parent provider exists (covered by the existing
370
+ * "allow nested provider to override" test above).
371
+ * Behavior B — state changes in the inner provider propagate outward so
372
+ * that hooks reading from an outer provider stay in sync.
373
+ *
374
+ * Scenarios mirror the reproduction cases in:
375
+ * https://github.com/CopilotKit/deep-agent-cpk-experiments/tree/main/app/client/src/tickets/tkt-modal-default-open
376
+ */
377
+ describe("Bidirectional sync (CPK-7152)", () => {
378
+ // Reusable probe/control component that reads the closest provider.
379
+ function ModalControls({ id }: { id: string }) {
380
+ const config = useCopilotChatConfiguration();
381
+ return (
382
+ <>
383
+ <div data-testid={`${id}-state`}>{String(config?.isModalOpen)}</div>
384
+ <button
385
+ data-testid={`${id}-open`}
386
+ onClick={() => config?.setModalOpen(true)}
387
+ >
388
+ open
389
+ </button>
390
+ <button
391
+ data-testid={`${id}-close`}
392
+ onClick={() => config?.setModalOpen(false)}
393
+ >
394
+ close
395
+ </button>
396
+ </>
397
+ );
398
+ }
399
+
400
+ it("scenario-sidebar-outer-hook: inner setModalOpen propagates to outer hook (Behavior B)", () => {
401
+ // Abe.Hu's layout: outer bare provider, inner provider owns explicit state.
402
+ // Toggling via the inner provider should update the outer hook.
403
+ render(
404
+ <CopilotChatConfigurationProvider threadId="outer">
405
+ {/* OuterProbe sits outside the inner provider — reads outer context */}
406
+ <ModalControls id="outer" />
407
+ <CopilotChatConfigurationProvider
408
+ threadId="inner"
409
+ isModalDefaultOpen={true}
410
+ >
411
+ <ModalControls id="inner" />
412
+ </CopilotChatConfigurationProvider>
413
+ </CopilotChatConfigurationProvider>,
414
+ );
415
+
416
+ expect(screen.getByTestId("outer-state").textContent).toBe("true");
417
+ expect(screen.getByTestId("inner-state").textContent).toBe("true");
418
+
419
+ act(() => {
420
+ fireEvent.click(screen.getByTestId("inner-close"));
421
+ });
422
+
423
+ // Inner closed — outer hook must reflect the change.
424
+ expect(screen.getByTestId("inner-state").textContent).toBe("false");
425
+ expect(screen.getByTestId("outer-state").textContent).toBe("false");
426
+ });
427
+
428
+ it("scenario-sidebar-outer-hook: outer setModalOpen propagates to inner (parent→child sync)", () => {
429
+ // If the user calls setModalOpen from the outer hook, the inner
430
+ // provider (and therefore the sidebar) must respond.
431
+ render(
432
+ <CopilotChatConfigurationProvider
433
+ threadId="outer"
434
+ isModalDefaultOpen={false}
435
+ >
436
+ <ModalControls id="outer" />
437
+ <CopilotChatConfigurationProvider
438
+ threadId="inner"
439
+ isModalDefaultOpen={false}
440
+ >
441
+ <ModalControls id="inner" />
442
+ </CopilotChatConfigurationProvider>
443
+ </CopilotChatConfigurationProvider>,
444
+ );
445
+
446
+ expect(screen.getByTestId("outer-state").textContent).toBe("false");
447
+ expect(screen.getByTestId("inner-state").textContent).toBe("false");
448
+
449
+ act(() => {
450
+ fireEvent.click(screen.getByTestId("outer-open"));
451
+ });
452
+
453
+ // Outer opened — inner must follow.
454
+ expect(screen.getByTestId("outer-state").textContent).toBe("true");
455
+ expect(screen.getByTestId("inner-state").textContent).toBe("true");
456
+ });
457
+
458
+ it("scenario-nested-provider: three-level chain propagates through middle provider", () => {
459
+ // Mirrors the real provider stack:
460
+ // Provider 1 (user's outer, no isModalDefaultOpen)
461
+ // └── Provider 2 (CopilotChat's, no isModalDefaultOpen) — "middle"
462
+ // └── Provider 3 (CopilotSidebarView's, explicit isModalDefaultOpen)
463
+ //
464
+ // Toggling P3 must reach P1 even though P2 has no explicit default.
465
+ render(
466
+ <CopilotChatConfigurationProvider threadId="p1">
467
+ <ModalControls id="p1" />
468
+ <CopilotChatConfigurationProvider threadId="p2">
469
+ {/* p2 has no isModalDefaultOpen — proxies p1's state */}
470
+ <CopilotChatConfigurationProvider
471
+ threadId="p3"
472
+ isModalDefaultOpen={true}
473
+ >
474
+ <ModalControls id="p3" />
475
+ </CopilotChatConfigurationProvider>
476
+ </CopilotChatConfigurationProvider>
477
+ </CopilotChatConfigurationProvider>,
478
+ );
479
+
480
+ expect(screen.getByTestId("p1-state").textContent).toBe("true");
481
+ expect(screen.getByTestId("p3-state").textContent).toBe("true");
482
+
483
+ act(() => {
484
+ fireEvent.click(screen.getByTestId("p3-close"));
485
+ });
486
+
487
+ expect(screen.getByTestId("p3-state").textContent).toBe("false");
488
+ expect(screen.getByTestId("p1-state").textContent).toBe("false");
489
+ });
490
+
491
+ it("scenario-nested-provider: Behavior A still holds after sync fix (no regression)", () => {
492
+ // Explicit isModalDefaultOpen on a child must still override the
493
+ // parent's current value on initial render — the sync effect must
494
+ // not overwrite the child's own initial state.
495
+ render(
496
+ <CopilotChatConfigurationProvider
497
+ threadId="outer"
498
+ isModalDefaultOpen={true}
499
+ >
500
+ <ModalControls id="outer" />
501
+ <CopilotChatConfigurationProvider
502
+ threadId="inner"
503
+ isModalDefaultOpen={false}
504
+ >
505
+ <ModalControls id="inner" />
506
+ </CopilotChatConfigurationProvider>
507
+ </CopilotChatConfigurationProvider>,
508
+ );
509
+
510
+ // Inner must start closed despite outer being open.
511
+ expect(screen.getByTestId("outer-state").textContent).toBe("true");
512
+ expect(screen.getByTestId("inner-state").textContent).toBe("false");
513
+ });
514
+ });
515
+
516
+ describe("Nested providers", () => {
517
+ it("should handle multiple nested providers correctly", () => {
518
+ render(
519
+ <CopilotChatConfigurationProvider
520
+ threadId="outer-thread"
521
+ agentId="outer-agent"
522
+ labels={{ chatInputPlaceholder: "Outer" }}
523
+ >
524
+ <CopilotChatConfigurationProvider
525
+ threadId="middle-thread"
526
+ agentId="middle-agent"
527
+ labels={{ chatInputPlaceholder: "Middle" }}
528
+ >
529
+ <CopilotChatConfigurationProvider
530
+ threadId="inner-thread"
531
+ agentId="inner-agent"
532
+ labels={{ chatInputPlaceholder: "Inner" }}
533
+ >
534
+ <ConfigurationDisplay />
535
+ </CopilotChatConfigurationProvider>
536
+ </CopilotChatConfigurationProvider>
537
+ </CopilotChatConfigurationProvider>,
538
+ );
539
+
540
+ // Innermost provider should win
541
+ expect(screen.getByTestId("agentId").textContent).toBe("inner-agent");
542
+ expect(screen.getByTestId("threadId").textContent).toBe("inner-thread");
543
+ expect(screen.getByTestId("placeholder").textContent).toBe("Inner");
544
+ });
545
+ });
546
+ });
@@ -0,0 +1,101 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { render, screen, act, waitFor } from "@testing-library/react";
3
+ import React from "react";
4
+ import { CopilotKitProvider } from "../CopilotKitProvider";
5
+
6
+ /**
7
+ * These tests verify that the license banner is driven by the server-reported
8
+ * licenseStatus field in the /info response — not by client-side token verification.
9
+ */
10
+
11
+ function mockFetchWithLicenseStatus(licenseStatus?: string) {
12
+ return vi.fn().mockResolvedValue({
13
+ ok: true,
14
+ status: 200,
15
+ json: async () => ({
16
+ version: "1.0.0",
17
+ agents: {},
18
+ audioFileTranscriptionEnabled: false,
19
+ mode: "intelligence",
20
+ licenseStatus,
21
+ }),
22
+ });
23
+ }
24
+
25
+ let originalFetch: typeof globalThis.fetch;
26
+
27
+ beforeEach(() => {
28
+ originalFetch = globalThis.fetch;
29
+ });
30
+
31
+ afterEach(() => {
32
+ globalThis.fetch = originalFetch;
33
+ });
34
+
35
+ describe("CopilotKitProvider license (server-driven)", () => {
36
+ it("shows no_license banner when server reports 'none'", async () => {
37
+ globalThis.fetch = mockFetchWithLicenseStatus("none") as any;
38
+ render(
39
+ <CopilotKitProvider runtimeUrl="/api">
40
+ <div>child</div>
41
+ </CopilotKitProvider>,
42
+ );
43
+ await waitFor(() => {
44
+ expect(screen.getByText(/Powered by CopilotKit/)).toBeTruthy();
45
+ });
46
+ });
47
+
48
+ it("shows expired banner when server reports 'expired'", async () => {
49
+ globalThis.fetch = mockFetchWithLicenseStatus("expired") as any;
50
+ render(
51
+ <CopilotKitProvider runtimeUrl="/api">
52
+ <div>child</div>
53
+ </CopilotKitProvider>,
54
+ );
55
+ await waitFor(() => {
56
+ expect(screen.getByText(/expired/i)).toBeTruthy();
57
+ });
58
+ });
59
+
60
+ it("shows invalid banner when server reports 'invalid'", async () => {
61
+ globalThis.fetch = mockFetchWithLicenseStatus("invalid") as any;
62
+ render(
63
+ <CopilotKitProvider runtimeUrl="/api">
64
+ <div>child</div>
65
+ </CopilotKitProvider>,
66
+ );
67
+ await waitFor(() => {
68
+ expect(
69
+ screen.getByText(/Invalid CopilotKit license token/i),
70
+ ).toBeTruthy();
71
+ });
72
+ });
73
+
74
+ it("shows no banner when server reports 'valid'", async () => {
75
+ globalThis.fetch = mockFetchWithLicenseStatus("valid") as any;
76
+ render(
77
+ <CopilotKitProvider runtimeUrl="/api">
78
+ <div>child</div>
79
+ </CopilotKitProvider>,
80
+ );
81
+ // Wait for runtime connection to complete
82
+ await waitFor(() => {
83
+ expect(screen.queryByText(/Powered by CopilotKit/)).toBeNull();
84
+ expect(screen.queryByText(/expired/i)).toBeNull();
85
+ expect(screen.queryByText(/Invalid/i)).toBeNull();
86
+ });
87
+ });
88
+
89
+ it("shows no banner when licenseStatus is absent (non-intelligence mode)", async () => {
90
+ globalThis.fetch = mockFetchWithLicenseStatus(undefined) as any;
91
+ render(
92
+ <CopilotKitProvider runtimeUrl="/api">
93
+ <div>child</div>
94
+ </CopilotKitProvider>,
95
+ );
96
+ await waitFor(() => {
97
+ expect(screen.queryByText(/Powered by CopilotKit/)).toBeNull();
98
+ expect(screen.queryByText(/expired/i)).toBeNull();
99
+ });
100
+ });
101
+ });