@copilotkit/react-core 1.54.1 → 1.55.0-next.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +127 -116
- package/dist/copilotkit-B3Mb1yVE.cjs +7975 -0
- package/dist/copilotkit-B3Mb1yVE.cjs.map +1 -0
- package/dist/copilotkit-DBzgOMby.d.cts +2182 -0
- package/dist/copilotkit-DBzgOMby.d.cts.map +1 -0
- package/dist/copilotkit-DNYSFuz5.mjs +7562 -0
- package/dist/copilotkit-DNYSFuz5.mjs.map +1 -0
- package/dist/copilotkit-Dy5w3qEV.d.mts +2182 -0
- package/dist/copilotkit-Dy5w3qEV.d.mts.map +1 -0
- package/dist/index.cjs +27 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4 -5
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +1941 -35
- package/dist/index.umd.js.map +1 -1
- package/dist/v2/index.cjs +77 -7
- package/dist/v2/index.css +1 -2
- package/dist/v2/index.d.cts +6 -4
- package/dist/v2/index.d.mts +6 -4
- package/dist/v2/index.mjs +7 -4
- package/dist/v2/index.umd.js +5725 -24
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +37 -9
- package/scripts/scope-preflight.mjs +101 -0
- package/src/components/CopilotListeners.tsx +2 -6
- package/src/components/copilot-provider/copilot-messages.tsx +1 -1
- package/src/components/copilot-provider/copilotkit-props.tsx +1 -1
- package/src/components/copilot-provider/copilotkit.tsx +4 -4
- package/src/context/copilot-messages-context.tsx +1 -1
- package/src/hooks/__tests__/use-coagent-config.test.ts +2 -2
- package/src/hooks/__tests__/use-coagent-state-render.e2e.test.tsx +2 -2
- package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +3 -7
- package/src/hooks/__tests__/use-frontend-tool-available.test.tsx +1 -1
- package/src/hooks/__tests__/use-frontend-tool-remount.e2e.test.tsx +4 -4
- package/src/hooks/use-agent-nodename.ts +1 -1
- package/src/hooks/use-coagent-state-render-bridge.tsx +1 -4
- package/src/hooks/use-coagent.ts +1 -1
- package/src/hooks/use-configure-chat-suggestions.tsx +2 -2
- package/src/hooks/use-copilot-chat-suggestions.tsx +2 -2
- package/src/hooks/use-copilot-chat_internal.ts +2 -2
- package/src/hooks/use-copilot-readable.ts +1 -1
- package/src/hooks/use-frontend-tool.ts +2 -2
- package/src/hooks/use-human-in-the-loop.ts +2 -2
- package/src/hooks/use-langgraph-interrupt.ts +2 -5
- package/src/hooks/use-lazy-tool-renderer.tsx +1 -1
- package/src/hooks/use-render-tool-call.ts +1 -1
- package/src/lib/copilot-task.ts +1 -1
- package/src/setupTests.ts +18 -14
- package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +176 -0
- package/src/v2/__tests__/globalSetup.ts +14 -0
- package/src/v2/__tests__/setup.ts +93 -0
- package/src/v2/__tests__/utils/test-helpers.tsx +470 -0
- package/src/v2/a2ui/A2UIMessageRenderer.tsx +206 -0
- package/src/v2/components/CopilotKitInspector.tsx +50 -0
- package/src/v2/components/MCPAppsActivityRenderer.tsx +785 -0
- package/src/v2/components/WildcardToolCallRender.tsx +86 -0
- package/src/v2/components/__tests__/license-warning-banner.test.tsx +46 -0
- package/src/v2/components/chat/CopilotChat.tsx +431 -0
- package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +375 -0
- package/src/v2/components/chat/CopilotChatAudioRecorder.tsx +350 -0
- package/src/v2/components/chat/CopilotChatInput.tsx +1302 -0
- package/src/v2/components/chat/CopilotChatMessageView.tsx +556 -0
- package/src/v2/components/chat/CopilotChatReasoningMessage.tsx +252 -0
- package/src/v2/components/chat/CopilotChatSuggestionPill.tsx +59 -0
- package/src/v2/components/chat/CopilotChatSuggestionView.tsx +133 -0
- package/src/v2/components/chat/CopilotChatToggleButton.tsx +171 -0
- package/src/v2/components/chat/CopilotChatToolCallsView.tsx +40 -0
- package/src/v2/components/chat/CopilotChatUserMessage.tsx +388 -0
- package/src/v2/components/chat/CopilotChatView.tsx +598 -0
- package/src/v2/components/chat/CopilotModalHeader.tsx +129 -0
- package/src/v2/components/chat/CopilotPopup.tsx +81 -0
- package/src/v2/components/chat/CopilotPopupView.tsx +317 -0
- package/src/v2/components/chat/CopilotSidebar.tsx +76 -0
- package/src/v2/components/chat/CopilotSidebarView.tsx +255 -0
- package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.tsx +1113 -0
- package/src/v2/components/chat/__tests__/CopilotChat.onError.test.tsx +73 -0
- package/src/v2/components/chat/__tests__/CopilotChat.slots.e2e.test.tsx +432 -0
- package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +150 -0
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.slots.e2e.test.tsx +624 -0
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.tsx +702 -0
- package/src/v2/components/chat/__tests__/CopilotChatCssClasses.test.tsx +107 -0
- package/src/v2/components/chat/__tests__/CopilotChatInput.slots.e2e.test.tsx +929 -0
- package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +986 -0
- package/src/v2/components/chat/__tests__/CopilotChatMessageView.slots.e2e.test.tsx +1004 -0
- package/src/v2/components/chat/__tests__/CopilotChatMessageView.test.tsx +169 -0
- package/src/v2/components/chat/__tests__/CopilotChatSuggestionView.slots.e2e.test.tsx +530 -0
- package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.tsx +782 -0
- package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +2413 -0
- package/src/v2/components/chat/__tests__/CopilotChatUserMessage.slots.e2e.test.tsx +621 -0
- package/src/v2/components/chat/__tests__/CopilotChatView.onClick.e2e.test.tsx +853 -0
- package/src/v2/components/chat/__tests__/CopilotChatView.slots.e2e.test.tsx +1050 -0
- package/src/v2/components/chat/__tests__/CopilotModalHeader.slots.e2e.test.tsx +484 -0
- package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.tsx +612 -0
- package/src/v2/components/chat/__tests__/CopilotSidebarView.slots.e2e.test.tsx +502 -0
- package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +1011 -0
- package/src/v2/components/chat/__tests__/setup.ts +1 -0
- package/src/v2/components/chat/index.ts +79 -0
- package/src/v2/components/index.ts +7 -0
- package/src/v2/components/license-warning-banner.tsx +198 -0
- package/src/v2/components/ui/button.tsx +123 -0
- package/src/v2/components/ui/dropdown-menu.tsx +258 -0
- package/src/v2/components/ui/tooltip.tsx +60 -0
- package/src/v2/hooks/__tests__/standard-schema-types.test.tsx +152 -0
- package/src/v2/hooks/__tests__/standard-schema.test.tsx +282 -0
- package/src/v2/hooks/__tests__/use-agent-context-timing.e2e.test.tsx +132 -0
- package/src/v2/hooks/__tests__/use-agent-context.test.tsx +401 -0
- package/src/v2/hooks/__tests__/use-agent-error-state.test.tsx +44 -0
- package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +205 -0
- package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +148 -0
- package/src/v2/hooks/__tests__/use-component.test.tsx +123 -0
- package/src/v2/hooks/__tests__/use-configure-suggestions.e2e.test.tsx +696 -0
- package/src/v2/hooks/__tests__/use-default-render-tool.test.tsx +153 -0
- package/src/v2/hooks/__tests__/use-frontend-tool-available.test.tsx +167 -0
- package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +2129 -0
- package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.tsx +1261 -0
- package/src/v2/hooks/__tests__/use-interrupt.test.tsx +397 -0
- package/src/v2/hooks/__tests__/use-katex-styles.test.tsx +56 -0
- package/src/v2/hooks/__tests__/use-keyboard-height.test.tsx +192 -0
- package/src/v2/hooks/__tests__/use-render-tool.test.tsx +259 -0
- package/src/v2/hooks/__tests__/use-suggestions.e2e.test.tsx +524 -0
- package/src/v2/hooks/__tests__/use-threads.test.tsx +433 -0
- package/src/v2/hooks/__tests__/zod-regression.test.tsx +311 -0
- package/src/v2/hooks/index.ts +18 -0
- package/src/v2/hooks/use-agent-context.tsx +45 -0
- package/src/v2/hooks/use-agent.tsx +155 -0
- package/src/v2/hooks/use-component.tsx +89 -0
- package/src/v2/hooks/use-configure-suggestions.tsx +187 -0
- package/src/v2/hooks/use-default-render-tool.tsx +254 -0
- package/src/v2/hooks/use-frontend-tool.tsx +43 -0
- package/src/v2/hooks/use-human-in-the-loop.tsx +81 -0
- package/src/v2/hooks/use-interrupt.tsx +305 -0
- package/src/v2/hooks/use-keyboard-height.tsx +67 -0
- package/src/v2/hooks/use-render-activity-message.tsx +73 -0
- package/src/v2/hooks/use-render-custom-messages.tsx +93 -0
- package/src/v2/hooks/use-render-tool-call.tsx +175 -0
- package/src/v2/hooks/use-render-tool.tsx +181 -0
- package/src/v2/hooks/use-suggestions.tsx +91 -0
- package/src/v2/hooks/use-threads.tsx +256 -0
- package/src/v2/hooks/useKatexStyles.ts +27 -0
- package/src/v2/index.css +1 -1
- package/src/v2/index.ts +18 -2
- package/src/v2/lib/__tests__/completePartialMarkdown.test.ts +495 -0
- package/src/v2/lib/__tests__/renderSlot.test.tsx +588 -0
- package/src/v2/lib/react-core.ts +156 -0
- package/src/v2/lib/slots.tsx +143 -0
- package/src/v2/lib/transcription-client.ts +184 -0
- package/src/v2/lib/utils.ts +8 -0
- package/src/v2/providers/CopilotChatConfigurationProvider.tsx +162 -0
- package/src/v2/providers/CopilotKitProvider.tsx +600 -0
- package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.tsx +546 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.license.test.tsx +101 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.onError.test.tsx +69 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.renderCustomMessages.e2e.test.tsx +881 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.stability.test.tsx +740 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +642 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.wildcard.test.tsx +294 -0
- package/src/v2/providers/index.ts +14 -0
- package/src/v2/styles/globals.css +230 -0
- package/src/v2/types/__tests__/defineToolCallRenderer.test.tsx +525 -0
- package/src/v2/types/defineToolCallRenderer.ts +65 -0
- package/src/v2/types/frontend-tool.ts +8 -0
- package/src/v2/types/human-in-the-loop.ts +33 -0
- package/src/v2/types/index.ts +7 -0
- package/src/v2/types/interrupt.ts +15 -0
- package/src/v2/types/react-activity-message-renderer.ts +27 -0
- package/src/v2/types/react-custom-message-renderer.ts +17 -0
- package/src/v2/types/react-tool-call-renderer.ts +32 -0
- package/tsdown.config.ts +34 -10
- package/vitest.config.mjs +4 -3
- package/LICENSE +0 -21
- package/dist/copilotkit-BRPQ2sqS.d.cts +0 -670
- package/dist/copilotkit-BRPQ2sqS.d.cts.map +0 -1
- package/dist/copilotkit-C94ayZbs.cjs +0 -2161
- package/dist/copilotkit-C94ayZbs.cjs.map +0 -1
- package/dist/copilotkit-CwZMFmSK.d.mts +0 -670
- package/dist/copilotkit-CwZMFmSK.d.mts.map +0 -1
- package/dist/copilotkit-Yh_Ld_FX.mjs +0 -2031
- package/dist/copilotkit-Yh_Ld_FX.mjs.map +0 -1
- 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
|
+
});
|