@copilotkit/react-core 1.56.3 → 1.56.4-canary.1777531098
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/dist/copilotkit-DFaI4j2r.d.mts.map +1 -1
- package/dist/{copilotkit-By2G6-Zx.cjs → copilotkit-DMFu29Kx.cjs} +142 -103
- package/dist/copilotkit-DMFu29Kx.cjs.map +1 -0
- package/dist/copilotkit-Dg4r4Gi_.d.cts.map +1 -1
- package/dist/{copilotkit-PzJlPKcU.mjs → copilotkit-OmIUrWym.mjs} +142 -103
- package/dist/copilotkit-OmIUrWym.mjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.umd.js +24 -0
- package/dist/index.umd.js.map +1 -1
- package/dist/v2/index.cjs +1 -1
- package/dist/v2/index.css +1 -1
- package/dist/v2/index.mjs +1 -1
- package/dist/v2/index.umd.js +144 -105
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +8 -8
- package/src/v2/components/chat/CopilotChatAttachmentQueue.tsx +7 -114
- package/src/v2/components/chat/CopilotChatAttachmentRenderer.tsx +26 -6
- package/src/v2/components/chat/CopilotChatUserMessage.tsx +2 -2
- package/src/v2/components/chat/CopilotChatView.tsx +66 -77
- package/src/v2/components/chat/Lightbox.tsx +103 -0
- package/src/v2/components/chat/__tests__/CopilotChat.suggestionsAlways.test.tsx +183 -0
- package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +193 -57
- package/src/v2/components/chat/__tests__/CopilotChatView.inputOverlay.test.tsx +264 -0
- package/src/v2/hooks/use-configure-suggestions.tsx +43 -0
- package/dist/copilotkit-By2G6-Zx.cjs.map +0 -1
- package/dist/copilotkit-PzJlPKcU.mjs.map +0 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { render, waitFor } from "@testing-library/react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { CopilotKitProvider } from "../../../providers/CopilotKitProvider";
|
|
5
|
+
import { CopilotChatConfigurationProvider } from "../../../providers/CopilotChatConfigurationProvider";
|
|
6
|
+
import { CopilotChatView } from "../CopilotChatView";
|
|
7
|
+
import { LastUserMessageContext } from "../last-user-message-context";
|
|
8
|
+
import type { Attachment } from "@copilotkit/shared";
|
|
9
|
+
import type { Message } from "@ag-ui/core";
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
HTMLElement.prototype.scrollTo = vi.fn();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
16
|
+
<CopilotKitProvider>
|
|
17
|
+
<CopilotChatConfigurationProvider threadId="test-thread">
|
|
18
|
+
<div style={{ height: 400 }}>{children}</div>
|
|
19
|
+
</CopilotChatConfigurationProvider>
|
|
20
|
+
</CopilotKitProvider>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const sampleMessages = [
|
|
24
|
+
{ id: "1", role: "user" as const, content: "Hello" },
|
|
25
|
+
{ id: "2", role: "assistant" as const, content: "Hi there!" },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const sampleAttachments: Attachment[] = [
|
|
29
|
+
{
|
|
30
|
+
id: "att-1",
|
|
31
|
+
type: "document",
|
|
32
|
+
source: {
|
|
33
|
+
type: "url",
|
|
34
|
+
value: "https://example.com/doc.txt",
|
|
35
|
+
mimeType: "text/plain",
|
|
36
|
+
},
|
|
37
|
+
filename: "example.txt",
|
|
38
|
+
size: 42,
|
|
39
|
+
status: "ready",
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
async function waitForMount(screen: {
|
|
44
|
+
findByTestId: (id: string) => Promise<HTMLElement>;
|
|
45
|
+
}) {
|
|
46
|
+
await screen.findByTestId("copilot-message-list");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("CopilotChatView input overlay layout", () => {
|
|
50
|
+
it("renders the input inside an absolute-positioned overlay wrapper on the main view", async () => {
|
|
51
|
+
const screen = render(
|
|
52
|
+
<TestWrapper>
|
|
53
|
+
<LastUserMessageContext.Provider value={{ id: null, sendNonce: 0 }}>
|
|
54
|
+
<CopilotChatView messages={sampleMessages} />
|
|
55
|
+
</LastUserMessageContext.Provider>
|
|
56
|
+
</TestWrapper>,
|
|
57
|
+
);
|
|
58
|
+
await waitForMount(screen);
|
|
59
|
+
|
|
60
|
+
// getByTestId throws if missing — presence is implicit.
|
|
61
|
+
const overlay = screen.getByTestId("copilot-input-overlay");
|
|
62
|
+
// Class-level assertion — the cpk: prefix avoids false positives from
|
|
63
|
+
// consumer classes. Absolute + bottom-0 is the contract we care about.
|
|
64
|
+
expect(overlay.className).toMatch(/cpk:absolute/);
|
|
65
|
+
expect(overlay.className).toMatch(/cpk:bottom-0/);
|
|
66
|
+
|
|
67
|
+
// Input (send button) lives inside the overlay, not outside it.
|
|
68
|
+
const sendButton = screen.getByTestId("copilot-send-button");
|
|
69
|
+
expect(overlay.contains(sendButton)).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("renders the attachment queue above the input inside the overlay wrapper", async () => {
|
|
73
|
+
const screen = render(
|
|
74
|
+
<TestWrapper>
|
|
75
|
+
<LastUserMessageContext.Provider value={{ id: null, sendNonce: 0 }}>
|
|
76
|
+
<CopilotChatView
|
|
77
|
+
messages={sampleMessages}
|
|
78
|
+
attachments={sampleAttachments}
|
|
79
|
+
/>
|
|
80
|
+
</LastUserMessageContext.Provider>
|
|
81
|
+
</TestWrapper>,
|
|
82
|
+
);
|
|
83
|
+
await waitForMount(screen);
|
|
84
|
+
|
|
85
|
+
const overlay = screen.getByTestId("copilot-input-overlay");
|
|
86
|
+
const queue = overlay.querySelector(
|
|
87
|
+
'[data-testid="copilot-attachment-queue"]',
|
|
88
|
+
);
|
|
89
|
+
const sendButton = overlay.querySelector(
|
|
90
|
+
'[data-testid="copilot-send-button"]',
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(queue).not.toBeNull();
|
|
94
|
+
expect(sendButton).not.toBeNull();
|
|
95
|
+
|
|
96
|
+
// DOM order: the attachment queue must appear before the send button
|
|
97
|
+
// in document order so it renders visually above the pill.
|
|
98
|
+
const position = queue!.compareDocumentPosition(sendButton!);
|
|
99
|
+
expect(position & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("does NOT wrap the welcome-screen input in the overlay", async () => {
|
|
103
|
+
const screen = render(
|
|
104
|
+
<TestWrapper>
|
|
105
|
+
<LastUserMessageContext.Provider value={{ id: null, sendNonce: 0 }}>
|
|
106
|
+
<CopilotChatView messages={[]} />
|
|
107
|
+
</LastUserMessageContext.Provider>
|
|
108
|
+
</TestWrapper>,
|
|
109
|
+
);
|
|
110
|
+
await screen.findByTestId("copilot-welcome-screen");
|
|
111
|
+
|
|
112
|
+
// Welcome screen present → no overlay wrapper exists in this render.
|
|
113
|
+
expect(screen.queryByTestId("copilot-input-overlay")).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("reserves inputContainerHeight as bottom padding on the scroll content", async () => {
|
|
117
|
+
// Spy on ResizeObserver so we can trigger a known height. The component
|
|
118
|
+
// uses ResizeObserver to measure the overlay wrapper; we inject a known
|
|
119
|
+
// value and assert the scroll content's inline padding-bottom reflects it.
|
|
120
|
+
const callbacks: Array<{
|
|
121
|
+
cb: ResizeObserverCallback;
|
|
122
|
+
target: Element | null;
|
|
123
|
+
}> = [];
|
|
124
|
+
const OriginalRO = global.ResizeObserver;
|
|
125
|
+
class MockResizeObserver {
|
|
126
|
+
private cb: ResizeObserverCallback;
|
|
127
|
+
constructor(cb: ResizeObserverCallback) {
|
|
128
|
+
this.cb = cb;
|
|
129
|
+
}
|
|
130
|
+
observe(target: Element) {
|
|
131
|
+
callbacks.push({ cb: this.cb, target });
|
|
132
|
+
}
|
|
133
|
+
unobserve() {}
|
|
134
|
+
disconnect() {}
|
|
135
|
+
}
|
|
136
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
137
|
+
(global as any).ResizeObserver = MockResizeObserver as any;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const screen = render(
|
|
141
|
+
<TestWrapper>
|
|
142
|
+
<LastUserMessageContext.Provider value={{ id: null, sendNonce: 0 }}>
|
|
143
|
+
<CopilotChatView messages={sampleMessages} />
|
|
144
|
+
</LastUserMessageContext.Provider>
|
|
145
|
+
</TestWrapper>,
|
|
146
|
+
);
|
|
147
|
+
await waitForMount(screen);
|
|
148
|
+
|
|
149
|
+
const scrollContent = screen.getByTestId("copilot-scroll-content");
|
|
150
|
+
|
|
151
|
+
// Simulate the overlay wrapper reporting a content height of 120px.
|
|
152
|
+
for (const { cb } of callbacks) {
|
|
153
|
+
cb(
|
|
154
|
+
[
|
|
155
|
+
{
|
|
156
|
+
contentRect: { height: 120 } as DOMRectReadOnly,
|
|
157
|
+
} as ResizeObserverEntry,
|
|
158
|
+
],
|
|
159
|
+
{} as ResizeObserver,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// After the resize fires, paddingBottom = 120 (input) + 32 (baseline,
|
|
164
|
+
// no suggestions) = "152px". The test asserts the formula.
|
|
165
|
+
await waitFor(() =>
|
|
166
|
+
expect(scrollContent.style.paddingBottom).toBe("152px"),
|
|
167
|
+
);
|
|
168
|
+
} finally {
|
|
169
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
170
|
+
(global as any).ResizeObserver = OriginalRO;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("attaches the resize observer when transitioning from welcome to chat view", async () => {
|
|
175
|
+
// Regression: a `[]`-deps useEffect captured `inputContainerRef.current`
|
|
176
|
+
// as null when mounted on the welcome screen and never re-ran after the
|
|
177
|
+
// user sent their first message. The overlay rendered without a measured
|
|
178
|
+
// height, so paddingBottom stayed at 32 and the last messages slid
|
|
179
|
+
// underneath the absolute-positioned input pill. Verify the observer
|
|
180
|
+
// attaches reactively when the overlay mounts post-transition.
|
|
181
|
+
const callbacks: Array<{
|
|
182
|
+
cb: ResizeObserverCallback;
|
|
183
|
+
target: Element | null;
|
|
184
|
+
}> = [];
|
|
185
|
+
const OriginalRO = global.ResizeObserver;
|
|
186
|
+
class MockResizeObserver {
|
|
187
|
+
private cb: ResizeObserverCallback;
|
|
188
|
+
constructor(cb: ResizeObserverCallback) {
|
|
189
|
+
this.cb = cb;
|
|
190
|
+
}
|
|
191
|
+
observe(target: Element) {
|
|
192
|
+
callbacks.push({ cb: this.cb, target });
|
|
193
|
+
}
|
|
194
|
+
unobserve() {}
|
|
195
|
+
disconnect() {}
|
|
196
|
+
}
|
|
197
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
198
|
+
(global as any).ResizeObserver = MockResizeObserver as any;
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
// Render with no messages to start on the welcome screen branch — the
|
|
202
|
+
// overlay wrapper does not exist in this DOM, so the observer cannot
|
|
203
|
+
// attach yet.
|
|
204
|
+
const initialMessages: Message[] = [];
|
|
205
|
+
const screen = render(
|
|
206
|
+
<TestWrapper>
|
|
207
|
+
<LastUserMessageContext.Provider value={{ id: null, sendNonce: 0 }}>
|
|
208
|
+
<CopilotChatView messages={initialMessages} />
|
|
209
|
+
</LastUserMessageContext.Provider>
|
|
210
|
+
</TestWrapper>,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
await screen.findByTestId("copilot-welcome-screen");
|
|
214
|
+
expect(screen.queryByTestId("copilot-input-overlay")).toBeNull();
|
|
215
|
+
|
|
216
|
+
// Transition to the chat view by re-rendering with messages — mirrors
|
|
217
|
+
// what happens when CopilotChat re-renders after the user submits.
|
|
218
|
+
screen.rerender(
|
|
219
|
+
<TestWrapper>
|
|
220
|
+
<LastUserMessageContext.Provider value={{ id: null, sendNonce: 0 }}>
|
|
221
|
+
<CopilotChatView messages={sampleMessages} />
|
|
222
|
+
</LastUserMessageContext.Provider>
|
|
223
|
+
</TestWrapper>,
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
await waitForMount(screen);
|
|
227
|
+
const overlay = screen.getByTestId("copilot-input-overlay");
|
|
228
|
+
|
|
229
|
+
// The bug: observer was attached at mount when the overlay element was
|
|
230
|
+
// null, so it never re-attached after the transition. Verify it now
|
|
231
|
+
// observes the overlay specifically.
|
|
232
|
+
await waitFor(() =>
|
|
233
|
+
expect(callbacks.some(({ target }) => target === overlay)).toBe(true),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const scrollContent = screen.getByTestId("copilot-scroll-content");
|
|
237
|
+
// Simulate the overlay reporting a real height (e.g. 88px input pill).
|
|
238
|
+
// Only fire on the overlay's own observer — other components (e.g. the
|
|
239
|
+
// textarea autosize) also use ResizeObserver and would corrupt the
|
|
240
|
+
// assertion if we fed all observers a 88px contentRect.
|
|
241
|
+
for (const { cb, target } of callbacks) {
|
|
242
|
+
if (target !== overlay) continue;
|
|
243
|
+
cb(
|
|
244
|
+
[
|
|
245
|
+
{
|
|
246
|
+
contentRect: { height: 88 } as DOMRectReadOnly,
|
|
247
|
+
} as ResizeObserverEntry,
|
|
248
|
+
],
|
|
249
|
+
{} as ResizeObserver,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 88 (input) + 32 (no suggestions baseline) = 120px. Without the fix,
|
|
254
|
+
// paddingBottom would be stuck at 32px because the observer never
|
|
255
|
+
// attached.
|
|
256
|
+
await waitFor(() =>
|
|
257
|
+
expect(scrollContent.style.paddingBottom).toBe("120px"),
|
|
258
|
+
);
|
|
259
|
+
} finally {
|
|
260
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
261
|
+
(global as any).ResizeObserver = OriginalRO;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -106,22 +106,37 @@ export function useConfigureSuggestions(
|
|
|
106
106
|
const isGlobalConfig =
|
|
107
107
|
rawConsumerAgentId === undefined || rawConsumerAgentId === "*";
|
|
108
108
|
|
|
109
|
+
const isDynamicConfigType = useMemo(
|
|
110
|
+
() => !!normalizedConfig && "instructions" in normalizedConfig,
|
|
111
|
+
[normalizedConfig],
|
|
112
|
+
);
|
|
113
|
+
|
|
109
114
|
const requestReload = useCallback(() => {
|
|
110
115
|
if (!normalizedConfig) {
|
|
111
116
|
return;
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
if (isGlobalConfig) {
|
|
120
|
+
const seen = new Set<string>();
|
|
115
121
|
const agents = Object.values(copilotkit.agents ?? {});
|
|
116
122
|
for (const entry of agents) {
|
|
117
123
|
const agentId = entry.agentId;
|
|
118
124
|
if (!agentId) {
|
|
119
125
|
continue;
|
|
120
126
|
}
|
|
127
|
+
seen.add(agentId);
|
|
121
128
|
if (!entry.isRunning) {
|
|
122
129
|
copilotkit.reloadSuggestions(agentId);
|
|
123
130
|
}
|
|
124
131
|
}
|
|
132
|
+
// Also reload for the chat's resolved consumer agent. The registry can
|
|
133
|
+
// be empty at this point (e.g. runtime info still loading), in which
|
|
134
|
+
// case the loop above wouldn't have fired for the agent the user is
|
|
135
|
+
// actually chatting with — and the welcome screen would render with
|
|
136
|
+
// no suggestions until they navigate away and back.
|
|
137
|
+
if (targetAgentId && !seen.has(targetAgentId)) {
|
|
138
|
+
copilotkit.reloadSuggestions(targetAgentId);
|
|
139
|
+
}
|
|
125
140
|
return;
|
|
126
141
|
}
|
|
127
142
|
|
|
@@ -169,6 +184,34 @@ export function useConfigureSuggestions(
|
|
|
169
184
|
}
|
|
170
185
|
requestReload();
|
|
171
186
|
}, [extraDeps.length, normalizedConfig, requestReload, ...extraDeps]);
|
|
187
|
+
|
|
188
|
+
// When agents arrive after the initial render (runtime info just landed),
|
|
189
|
+
// re-request a reload so dynamic configs that need a real agent can finally
|
|
190
|
+
// generate. Skip for static configs — they don't need an agent and the
|
|
191
|
+
// initial mount reload already handled them. Skip when the target agent
|
|
192
|
+
// is already in the registry — the initial reload already covered it, and
|
|
193
|
+
// re-firing on every subsequent `onAgentsChanged` (e.g. dev-mode hot
|
|
194
|
+
// reloads, sibling chat configs mounting) would stack overlapping
|
|
195
|
+
// generations.
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (!normalizedConfig || !isDynamicConfigType) return;
|
|
198
|
+
if (!targetAgentId) return;
|
|
199
|
+
|
|
200
|
+
const initiallyPresent = !!copilotkit.getAgent(targetAgentId);
|
|
201
|
+
if (initiallyPresent) return;
|
|
202
|
+
|
|
203
|
+
const subscription = copilotkit.subscribe({
|
|
204
|
+
onAgentsChanged: () => {
|
|
205
|
+
if (copilotkit.getAgent(targetAgentId)) {
|
|
206
|
+
requestReload();
|
|
207
|
+
subscription.unsubscribe();
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
return () => {
|
|
212
|
+
subscription.unsubscribe();
|
|
213
|
+
};
|
|
214
|
+
}, [copilotkit, normalizedConfig, isDynamicConfigType, targetAgentId, requestReload]);
|
|
172
215
|
}
|
|
173
216
|
|
|
174
217
|
function isDynamicConfig(
|