@copilotkit/react-core 1.56.0 → 1.56.2-canary.pin-to-send
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-BebqQrYT.mjs → copilotkit-BBYbekCa.mjs} +265 -76
- package/dist/copilotkit-BBYbekCa.mjs.map +1 -0
- package/dist/{copilotkit-Cvb6WpAX.cjs → copilotkit-D5JT2Pu3.cjs} +264 -75
- package/dist/copilotkit-D5JT2Pu3.cjs.map +1 -0
- package/dist/{copilotkit-f2Uq0RwG.d.mts → copilotkit-DArT2Iuw.d.mts} +71 -18
- package/dist/copilotkit-DArT2Iuw.d.mts.map +1 -0
- package/dist/{copilotkit-Dv8zU8_U.d.cts → copilotkit-KEc28l8G.d.cts} +71 -18
- package/dist/copilotkit-KEc28l8G.d.cts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.umd.js +30 -46
- 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.d.cts +2 -2
- package/dist/v2/index.d.mts +2 -2
- package/dist/v2/index.mjs +1 -1
- package/dist/v2/index.umd.js +264 -79
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +6 -6
- package/src/components/CopilotListeners.tsx +15 -4
- package/src/components/__tests__/CopilotListeners.test.tsx +38 -0
- package/src/v2/components/chat/CopilotChat.tsx +80 -4
- package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +4 -4
- package/src/v2/components/chat/CopilotChatInput.tsx +43 -2
- package/src/v2/components/chat/CopilotChatView.tsx +206 -11
- package/src/v2/components/chat/__tests__/CopilotChat.absentThreadConnect.test.tsx +66 -0
- package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +300 -2
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.thumbs.test.tsx +72 -0
- package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +38 -0
- package/src/v2/components/chat/__tests__/CopilotChatView.connectingGate.test.tsx +56 -0
- package/src/v2/components/chat/__tests__/CopilotChatView.pinToSend.test.tsx +94 -0
- package/src/v2/components/chat/__tests__/copilot-chat-throttle.test.tsx +0 -1
- package/src/v2/components/chat/__tests__/normalize-auto-scroll.test.ts +37 -0
- package/src/v2/components/chat/index.ts +2 -0
- package/src/v2/components/chat/last-user-message-context.ts +21 -0
- package/src/v2/components/chat/normalize-auto-scroll.ts +17 -0
- package/src/v2/components/license-warning-banner.tsx +20 -1
- package/src/v2/components/ui/button.tsx +12 -11
- package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +6 -0
- package/src/v2/hooks/__tests__/use-agent-thread-isolation.test.tsx +6 -0
- package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +76 -50
- package/src/v2/hooks/__tests__/use-pin-to-send.test.tsx +219 -0
- package/src/v2/hooks/__tests__/use-render-custom-messages.test.tsx +55 -0
- package/src/v2/hooks/__tests__/use-threads.test.tsx +68 -0
- package/src/v2/hooks/use-agent.tsx +34 -77
- package/src/v2/hooks/use-pin-to-send.ts +94 -0
- package/src/v2/hooks/use-render-custom-messages.tsx +1 -1
- package/src/v2/hooks/use-render-tool-call.tsx +3 -0
- package/src/v2/hooks/use-render-tool.tsx +3 -0
- package/src/v2/hooks/use-threads.tsx +55 -12
- package/src/v2/providers/CopilotKitProvider.tsx +2 -11
- package/src/v2/types/defineToolCallRenderer.ts +3 -0
- package/src/v2/types/react-tool-call-renderer.ts +3 -0
- package/dist/copilotkit-BebqQrYT.mjs.map +0 -1
- package/dist/copilotkit-Cvb6WpAX.cjs.map +0 -1
- package/dist/copilotkit-Dv8zU8_U.d.cts.map +0 -1
- package/dist/copilotkit-f2Uq0RwG.d.mts.map +0 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import {
|
|
5
|
+
MockReconnectableAgent,
|
|
5
6
|
MockStepwiseAgent,
|
|
6
7
|
activitySnapshotEvent,
|
|
7
8
|
renderWithCopilotKit,
|
|
@@ -10,9 +11,48 @@ import {
|
|
|
10
11
|
testId,
|
|
11
12
|
} from "../../../__tests__/utils/test-helpers";
|
|
12
13
|
import { ReactActivityMessageRenderer } from "../../../types";
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
CopilotChatConfigurationProvider,
|
|
16
|
+
CopilotKitProvider,
|
|
17
|
+
useCopilotKit,
|
|
18
|
+
} from "../../../providers";
|
|
14
19
|
import { AbstractAgent } from "@ag-ui/client";
|
|
20
|
+
import { IntelligenceAgent } from "@copilotkit/core";
|
|
15
21
|
import { getThreadClone } from "../../../hooks/use-agent";
|
|
22
|
+
import { createA2UIMessageRenderer } from "../../../a2ui/A2UIMessageRenderer";
|
|
23
|
+
import type { Theme } from "@copilotkit/a2ui-renderer";
|
|
24
|
+
import { CopilotChat } from "..";
|
|
25
|
+
|
|
26
|
+
const { mockWebsandboxCreate, mockWebsandboxDestroy } = vi.hoisted(() => {
|
|
27
|
+
const mockDestroy = vi.fn();
|
|
28
|
+
const mockCreate = vi.fn(() => ({
|
|
29
|
+
iframe: document.createElement("iframe"),
|
|
30
|
+
promise: Promise.resolve(),
|
|
31
|
+
run: vi.fn().mockResolvedValue(undefined),
|
|
32
|
+
destroy: mockDestroy,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
mockWebsandboxCreate: mockCreate,
|
|
37
|
+
mockWebsandboxDestroy: mockDestroy,
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
vi.mock("@jetbrains/websandbox", () => ({
|
|
42
|
+
default: {
|
|
43
|
+
create: (...args: unknown[]) => mockWebsandboxCreate(...args),
|
|
44
|
+
},
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
function jsonResponse(body: unknown, status = 200): Response {
|
|
48
|
+
return {
|
|
49
|
+
ok: status >= 200 && status < 300,
|
|
50
|
+
status,
|
|
51
|
+
statusText: status === 200 ? "OK" : "Error",
|
|
52
|
+
json: async () => body,
|
|
53
|
+
text: async () => JSON.stringify(body),
|
|
54
|
+
} as Response;
|
|
55
|
+
}
|
|
16
56
|
|
|
17
57
|
describe("CopilotChat activity message rendering", () => {
|
|
18
58
|
it("renders custom components for activity snapshots", async () => {
|
|
@@ -208,4 +248,262 @@ describe("CopilotChat activity message rendering", () => {
|
|
|
208
248
|
expect(capturedAgent).toBe(clone);
|
|
209
249
|
expect(capturedAgent).not.toBe(agent); // must NOT be the registry agent
|
|
210
250
|
});
|
|
251
|
+
|
|
252
|
+
it("restores a completed A2UI surface after reconnect from an event-native baseline", async () => {
|
|
253
|
+
const agent = new MockReconnectableAgent();
|
|
254
|
+
const threadId = testId("a2ui-thread");
|
|
255
|
+
const surfaceId = testId("surface");
|
|
256
|
+
const a2uiRenderer = createA2UIMessageRenderer({
|
|
257
|
+
theme: {} as Theme,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const { unmount } = renderWithCopilotKit({
|
|
261
|
+
agent,
|
|
262
|
+
threadId,
|
|
263
|
+
renderActivityMessages: [a2uiRenderer],
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const input = await screen.findByRole("textbox");
|
|
267
|
+
fireEvent.change(input, { target: { value: "Show me the restored UI" } });
|
|
268
|
+
fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
|
|
269
|
+
|
|
270
|
+
await waitFor(() => {
|
|
271
|
+
expect(screen.getByText("Show me the restored UI")).toBeDefined();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
agent.emit(runStartedEvent());
|
|
275
|
+
agent.emit(
|
|
276
|
+
activitySnapshotEvent({
|
|
277
|
+
messageId: testId("a2ui-activity"),
|
|
278
|
+
activityType: "a2ui-surface",
|
|
279
|
+
content: {
|
|
280
|
+
a2ui_operations: [
|
|
281
|
+
{
|
|
282
|
+
version: "v0.9",
|
|
283
|
+
createSurface: {
|
|
284
|
+
surfaceId,
|
|
285
|
+
catalogId:
|
|
286
|
+
"https://a2ui.org/specification/v0_9/basic_catalog.json",
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
version: "v0.9",
|
|
291
|
+
updateComponents: {
|
|
292
|
+
surfaceId,
|
|
293
|
+
components: [
|
|
294
|
+
{
|
|
295
|
+
id: "root",
|
|
296
|
+
component: "Text",
|
|
297
|
+
text: "Restored dashboard",
|
|
298
|
+
variant: "body",
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
},
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
agent.emit(runFinishedEvent());
|
|
308
|
+
agent.complete();
|
|
309
|
+
|
|
310
|
+
await waitFor(() => {
|
|
311
|
+
expect(
|
|
312
|
+
document.querySelector(`[data-surface-id='${surfaceId}']`),
|
|
313
|
+
).not.toBeNull();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
unmount();
|
|
317
|
+
agent.reset();
|
|
318
|
+
|
|
319
|
+
renderWithCopilotKit({
|
|
320
|
+
agent,
|
|
321
|
+
threadId,
|
|
322
|
+
renderActivityMessages: [a2uiRenderer],
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
await waitFor(() => {
|
|
326
|
+
expect(
|
|
327
|
+
document.querySelector(`[data-surface-id='${surfaceId}']`),
|
|
328
|
+
).not.toBeNull();
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("restores a completed A2UI surface from an IntelligenceAgent /connect bootstrap plan", async () => {
|
|
333
|
+
const threadId = testId("intelligence-connect-thread");
|
|
334
|
+
const surfaceId = testId("intelligence-connect-surface");
|
|
335
|
+
const fetchMock = vi.fn().mockResolvedValueOnce(
|
|
336
|
+
jsonResponse({
|
|
337
|
+
mode: "bootstrap",
|
|
338
|
+
latestEventId: "event-3",
|
|
339
|
+
events: [
|
|
340
|
+
{
|
|
341
|
+
type: "RUN_STARTED",
|
|
342
|
+
threadId,
|
|
343
|
+
run_id: "backend-run-1",
|
|
344
|
+
input: {
|
|
345
|
+
messages: [
|
|
346
|
+
{
|
|
347
|
+
id: testId("connect-user-message"),
|
|
348
|
+
role: "user",
|
|
349
|
+
content: "show me the restored ui",
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
type: "ACTIVITY_SNAPSHOT",
|
|
356
|
+
messageId: testId("connect-a2ui-activity"),
|
|
357
|
+
activityType: "a2ui-surface",
|
|
358
|
+
content: {
|
|
359
|
+
a2ui_operations: [
|
|
360
|
+
{
|
|
361
|
+
version: "v0.9",
|
|
362
|
+
createSurface: {
|
|
363
|
+
surfaceId,
|
|
364
|
+
catalogId:
|
|
365
|
+
"https://a2ui.org/specification/v0_9/basic_catalog.json",
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
version: "v0.9",
|
|
370
|
+
updateComponents: {
|
|
371
|
+
surfaceId,
|
|
372
|
+
components: [
|
|
373
|
+
{
|
|
374
|
+
id: "root",
|
|
375
|
+
component: "Text",
|
|
376
|
+
text: "Restored dashboard",
|
|
377
|
+
variant: "body",
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
type: "RUN_FINISHED",
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
}),
|
|
390
|
+
);
|
|
391
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
392
|
+
|
|
393
|
+
const agent = new IntelligenceAgent({
|
|
394
|
+
url: "ws://localhost:4000/client",
|
|
395
|
+
runtimeUrl: "http://localhost:4000",
|
|
396
|
+
agentId: "my-agent",
|
|
397
|
+
});
|
|
398
|
+
const a2uiRenderer = createA2UIMessageRenderer({
|
|
399
|
+
theme: {} as Theme,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
renderWithCopilotKit({
|
|
404
|
+
agent,
|
|
405
|
+
threadId,
|
|
406
|
+
renderActivityMessages: [a2uiRenderer],
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
await waitFor(() => {
|
|
410
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
411
|
+
});
|
|
412
|
+
await waitFor(() => {
|
|
413
|
+
expect(screen.getByText("show me the restored ui")).toBeDefined();
|
|
414
|
+
});
|
|
415
|
+
await waitFor(() => {
|
|
416
|
+
expect(
|
|
417
|
+
document.querySelector(`[data-surface-id='${surfaceId}']`),
|
|
418
|
+
).not.toBeNull();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const [url, options] = fetchMock.mock.calls[0] as [string, RequestInit];
|
|
422
|
+
expect(url).toContain("/agent/my-agent/connect");
|
|
423
|
+
expect(options.method).toBe("POST");
|
|
424
|
+
|
|
425
|
+
const requestBody = JSON.parse(String(options.body)) as {
|
|
426
|
+
threadId: string;
|
|
427
|
+
lastSeenEventId: string | null;
|
|
428
|
+
messages: unknown[];
|
|
429
|
+
};
|
|
430
|
+
expect(requestBody.threadId).toBe(threadId);
|
|
431
|
+
expect(requestBody.lastSeenEventId).toBeNull();
|
|
432
|
+
expect(requestBody.messages).toEqual([]);
|
|
433
|
+
} finally {
|
|
434
|
+
vi.unstubAllGlobals();
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("restores a completed Open Generative UI activity after reconnect from an event-native baseline", async () => {
|
|
439
|
+
mockWebsandboxCreate.mockClear();
|
|
440
|
+
mockWebsandboxDestroy.mockClear();
|
|
441
|
+
|
|
442
|
+
const agent = new MockReconnectableAgent();
|
|
443
|
+
const threadId = testId("open-generative-ui-thread");
|
|
444
|
+
const restoredHtml =
|
|
445
|
+
"<head></head><body><div>Restored open generative UI</div></body>";
|
|
446
|
+
|
|
447
|
+
const renderOpenGenerativeUIChat = () =>
|
|
448
|
+
render(
|
|
449
|
+
<CopilotKitProvider
|
|
450
|
+
agents__unsafe_dev_only={{ default: agent }}
|
|
451
|
+
openGenerativeUI={{}}
|
|
452
|
+
>
|
|
453
|
+
<CopilotChatConfigurationProvider threadId={threadId}>
|
|
454
|
+
<div style={{ height: 400 }}>
|
|
455
|
+
<CopilotChat welcomeScreen={false} />
|
|
456
|
+
</div>
|
|
457
|
+
</CopilotChatConfigurationProvider>
|
|
458
|
+
</CopilotKitProvider>,
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
const { unmount } = renderOpenGenerativeUIChat();
|
|
462
|
+
|
|
463
|
+
const input = await screen.findByRole("textbox");
|
|
464
|
+
fireEvent.change(input, { target: { value: "Show me the restored app" } });
|
|
465
|
+
fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
|
|
466
|
+
|
|
467
|
+
await waitFor(() => {
|
|
468
|
+
expect(screen.getByText("Show me the restored app")).toBeDefined();
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
agent.emit(runStartedEvent());
|
|
472
|
+
agent.emit(
|
|
473
|
+
activitySnapshotEvent({
|
|
474
|
+
messageId: testId("open-generative-ui-activity"),
|
|
475
|
+
activityType: "open-generative-ui",
|
|
476
|
+
content: {
|
|
477
|
+
initialHeight: 180,
|
|
478
|
+
generating: false,
|
|
479
|
+
html: [restoredHtml],
|
|
480
|
+
htmlComplete: true,
|
|
481
|
+
},
|
|
482
|
+
}),
|
|
483
|
+
);
|
|
484
|
+
agent.emit(runFinishedEvent());
|
|
485
|
+
agent.complete();
|
|
486
|
+
|
|
487
|
+
await waitFor(() => {
|
|
488
|
+
expect(mockWebsandboxCreate).toHaveBeenCalledTimes(1);
|
|
489
|
+
});
|
|
490
|
+
expect(mockWebsandboxCreate.mock.calls[0]?.[1]).toMatchObject({
|
|
491
|
+
frameContent: restoredHtml,
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
unmount();
|
|
495
|
+
|
|
496
|
+
agent.reset();
|
|
497
|
+
|
|
498
|
+
renderOpenGenerativeUIChat();
|
|
499
|
+
|
|
500
|
+
await waitFor(() => {
|
|
501
|
+
expect(mockWebsandboxCreate).toHaveBeenCalledTimes(2);
|
|
502
|
+
});
|
|
503
|
+
expect(mockWebsandboxCreate.mock.calls[1]?.[1]).toMatchObject({
|
|
504
|
+
frameContent: restoredHtml,
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
expect(mockWebsandboxDestroy).toHaveBeenCalledTimes(1);
|
|
508
|
+
});
|
|
211
509
|
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
3
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
4
|
+
import { AssistantMessage } from "@ag-ui/core";
|
|
5
|
+
import { CopilotChatAssistantMessage } from "../CopilotChatAssistantMessage";
|
|
6
|
+
import { CopilotChatConfigurationProvider } from "../../../providers/CopilotChatConfigurationProvider";
|
|
7
|
+
import { CopilotKitProvider } from "../../../providers/CopilotKitProvider";
|
|
8
|
+
|
|
9
|
+
const TEST_THREAD_ID = "test-thread";
|
|
10
|
+
|
|
11
|
+
const renderWithProvider = (component: React.ReactElement) => {
|
|
12
|
+
return render(
|
|
13
|
+
<CopilotKitProvider>
|
|
14
|
+
<CopilotChatConfigurationProvider threadId={TEST_THREAD_ID}>
|
|
15
|
+
{component}
|
|
16
|
+
</CopilotChatConfigurationProvider>
|
|
17
|
+
</CopilotKitProvider>,
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe("CopilotChatAssistantMessage thumbs callbacks (#3457)", () => {
|
|
22
|
+
const message: AssistantMessage = {
|
|
23
|
+
id: "msg-1",
|
|
24
|
+
role: "assistant",
|
|
25
|
+
content: "Hello from the assistant",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
it("onThumbsUp receives AssistantMessage, not SyntheticEvent", () => {
|
|
29
|
+
const onThumbsUp = vi.fn();
|
|
30
|
+
|
|
31
|
+
renderWithProvider(
|
|
32
|
+
<CopilotChatAssistantMessage message={message} onThumbsUp={onThumbsUp} />,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const thumbsUpButton = screen.getByRole("button", {
|
|
36
|
+
name: /good response/i,
|
|
37
|
+
});
|
|
38
|
+
fireEvent.click(thumbsUpButton);
|
|
39
|
+
|
|
40
|
+
expect(onThumbsUp).toHaveBeenCalledTimes(1);
|
|
41
|
+
const arg = onThumbsUp.mock.calls[0][0];
|
|
42
|
+
// Should receive AssistantMessage
|
|
43
|
+
expect(arg).toHaveProperty("id", "msg-1");
|
|
44
|
+
expect(arg).toHaveProperty("role", "assistant");
|
|
45
|
+
expect(arg).toHaveProperty("content", "Hello from the assistant");
|
|
46
|
+
// Should NOT receive a SyntheticEvent (which has nativeEvent, target, etc.)
|
|
47
|
+
expect(arg).not.toHaveProperty("nativeEvent");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("onThumbsDown receives AssistantMessage, not SyntheticEvent", () => {
|
|
51
|
+
const onThumbsDown = vi.fn();
|
|
52
|
+
|
|
53
|
+
renderWithProvider(
|
|
54
|
+
<CopilotChatAssistantMessage
|
|
55
|
+
message={message}
|
|
56
|
+
onThumbsDown={onThumbsDown}
|
|
57
|
+
/>,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const thumbsDownButton = screen.getByRole("button", {
|
|
61
|
+
name: /bad response/i,
|
|
62
|
+
});
|
|
63
|
+
fireEvent.click(thumbsDownButton);
|
|
64
|
+
|
|
65
|
+
expect(onThumbsDown).toHaveBeenCalledTimes(1);
|
|
66
|
+
const arg = onThumbsDown.mock.calls[0][0];
|
|
67
|
+
expect(arg).toHaveProperty("id", "msg-1");
|
|
68
|
+
expect(arg).toHaveProperty("role", "assistant");
|
|
69
|
+
expect(arg).toHaveProperty("content", "Hello from the assistant");
|
|
70
|
+
expect(arg).not.toHaveProperty("nativeEvent");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -985,6 +985,44 @@ describe("CopilotChatInput", () => {
|
|
|
985
985
|
expect((input as HTMLTextAreaElement).value).toBe("test message");
|
|
986
986
|
expect(mockOnSubmitMessage).toHaveBeenCalledWith("test message");
|
|
987
987
|
});
|
|
988
|
+
|
|
989
|
+
it("calls onChange with empty string after submission in controlled mode", () => {
|
|
990
|
+
const mockOnChange = vi.fn();
|
|
991
|
+
const mockOnSubmitMessage = vi.fn();
|
|
992
|
+
|
|
993
|
+
const { container } = renderWithProvider(
|
|
994
|
+
<CopilotChatInput
|
|
995
|
+
value="test message"
|
|
996
|
+
onChange={mockOnChange}
|
|
997
|
+
onSubmitMessage={mockOnSubmitMessage}
|
|
998
|
+
/>,
|
|
999
|
+
);
|
|
1000
|
+
|
|
1001
|
+
const sendButton = getSendButton(container);
|
|
1002
|
+
fireEvent.click(sendButton!);
|
|
1003
|
+
|
|
1004
|
+
expect(mockOnSubmitMessage).toHaveBeenCalledWith("test message");
|
|
1005
|
+
expect(mockOnChange).toHaveBeenCalledWith("");
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
it("calls onChange with empty string after Enter submission in controlled mode", () => {
|
|
1009
|
+
const mockOnChange = vi.fn();
|
|
1010
|
+
const mockOnSubmitMessage = vi.fn();
|
|
1011
|
+
|
|
1012
|
+
renderWithProvider(
|
|
1013
|
+
<CopilotChatInput
|
|
1014
|
+
value="hello world"
|
|
1015
|
+
onChange={mockOnChange}
|
|
1016
|
+
onSubmitMessage={mockOnSubmitMessage}
|
|
1017
|
+
/>,
|
|
1018
|
+
);
|
|
1019
|
+
|
|
1020
|
+
const input = screen.getByRole("textbox");
|
|
1021
|
+
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
|
1022
|
+
|
|
1023
|
+
expect(mockOnSubmitMessage).toHaveBeenCalledWith("hello world");
|
|
1024
|
+
expect(mockOnChange).toHaveBeenCalledWith("");
|
|
1025
|
+
});
|
|
988
1026
|
});
|
|
989
1027
|
|
|
990
1028
|
describe("Container dimension cache", () => {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { describe, it, expect } from "vitest";
|
|
4
|
+
import { CopilotChatView } from "../CopilotChatView";
|
|
5
|
+
import { CopilotKitProvider } from "../../../providers/CopilotKitProvider";
|
|
6
|
+
import { CopilotChatConfigurationProvider } from "../../../providers/CopilotChatConfigurationProvider";
|
|
7
|
+
|
|
8
|
+
// Minimal provider wrapper. No agent registry is required because these tests
|
|
9
|
+
// only exercise local render decisions (welcome-screen suppression) that
|
|
10
|
+
// don't touch the agent runtime.
|
|
11
|
+
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
12
|
+
<CopilotKitProvider>
|
|
13
|
+
<CopilotChatConfigurationProvider threadId="test-thread">
|
|
14
|
+
<div style={{ height: 400 }}>{children}</div>
|
|
15
|
+
</CopilotChatConfigurationProvider>
|
|
16
|
+
</CopilotKitProvider>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
describe("CopilotChatView connect-gating", () => {
|
|
20
|
+
it("suppresses the welcome screen while isConnecting=true", () => {
|
|
21
|
+
render(
|
|
22
|
+
<TestWrapper>
|
|
23
|
+
<CopilotChatView messages={[]} isConnecting />
|
|
24
|
+
</TestWrapper>,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// Switching threads would otherwise flash the welcome greeting before
|
|
28
|
+
// bootstrap messages arrive.
|
|
29
|
+
expect(screen.queryByTestId("copilot-welcome-screen")).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("suppresses the welcome screen when hasExplicitThreadId=true", () => {
|
|
33
|
+
render(
|
|
34
|
+
<TestWrapper>
|
|
35
|
+
<CopilotChatView messages={[]} hasExplicitThreadId />
|
|
36
|
+
</TestWrapper>,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// A caller-managed thread (threadId prop / config provider) should never
|
|
40
|
+
// display the generic "start a new chat" welcome — even when the thread
|
|
41
|
+
// has no messages yet.
|
|
42
|
+
expect(screen.queryByTestId("copilot-welcome-screen")).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("shows the welcome screen by default for a fresh empty chat", () => {
|
|
46
|
+
render(
|
|
47
|
+
<TestWrapper>
|
|
48
|
+
<CopilotChatView messages={[]} />
|
|
49
|
+
</TestWrapper>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Positive control: with no threadId supplied and no connect in flight,
|
|
53
|
+
// an empty chat should still render the welcome screen.
|
|
54
|
+
expect(screen.getByTestId("copilot-welcome-screen")).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { render } 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
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
HTMLElement.prototype.scrollTo = vi.fn();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Wrapper to provide required context (same pattern as CopilotChatView.slots.e2e.test.tsx)
|
|
14
|
+
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
15
|
+
<CopilotKitProvider>
|
|
16
|
+
<CopilotChatConfigurationProvider threadId="test-thread">
|
|
17
|
+
<div style={{ height: 400 }}>{children}</div>
|
|
18
|
+
</CopilotChatConfigurationProvider>
|
|
19
|
+
</CopilotKitProvider>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const sampleMessages = [
|
|
23
|
+
{ id: "1", role: "user" as const, content: "Hello" },
|
|
24
|
+
{ id: "2", role: "assistant" as const, content: "Hi there!" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Wait for the ScrollView's `hasMounted` useEffect to flip — the pre-mount
|
|
28
|
+
// fallback render does not include the message list, so a findBy on the
|
|
29
|
+
// message list is a reliable "mount is done" signal. Without this gate,
|
|
30
|
+
// absence assertions pass vacuously against the pre-mount render.
|
|
31
|
+
async function waitForMount(screen: {
|
|
32
|
+
findByTestId: (id: string) => Promise<HTMLElement>;
|
|
33
|
+
}) {
|
|
34
|
+
await screen.findByTestId("copilot-message-list");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("CopilotChatView pin-to-send mode", () => {
|
|
38
|
+
it("renders the pin-to-send spacer element when autoScroll='pin-to-send'", async () => {
|
|
39
|
+
const screen = render(
|
|
40
|
+
<TestWrapper>
|
|
41
|
+
<LastUserMessageContext.Provider value={{ id: null, sendNonce: 0 }}>
|
|
42
|
+
<CopilotChatView autoScroll="pin-to-send" messages={sampleMessages} />
|
|
43
|
+
</LastUserMessageContext.Provider>
|
|
44
|
+
</TestWrapper>,
|
|
45
|
+
);
|
|
46
|
+
await waitForMount(screen);
|
|
47
|
+
const spacer = screen.container.querySelector("[data-pin-to-send-spacer]");
|
|
48
|
+
expect(spacer).not.toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("does not render the spacer when autoScroll='pin-to-bottom'", async () => {
|
|
52
|
+
const screen = render(
|
|
53
|
+
<TestWrapper>
|
|
54
|
+
<CopilotChatView autoScroll="pin-to-bottom" messages={sampleMessages} />
|
|
55
|
+
</TestWrapper>,
|
|
56
|
+
);
|
|
57
|
+
await waitForMount(screen);
|
|
58
|
+
const spacer = screen.container.querySelector("[data-pin-to-send-spacer]");
|
|
59
|
+
expect(spacer).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("does not render the spacer when autoScroll='none'", async () => {
|
|
63
|
+
const screen = render(
|
|
64
|
+
<TestWrapper>
|
|
65
|
+
<CopilotChatView autoScroll="none" messages={sampleMessages} />
|
|
66
|
+
</TestWrapper>,
|
|
67
|
+
);
|
|
68
|
+
await waitForMount(screen);
|
|
69
|
+
const spacer = screen.container.querySelector("[data-pin-to-send-spacer]");
|
|
70
|
+
expect(spacer).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("boolean true still maps to pin-to-bottom (back-compat)", async () => {
|
|
74
|
+
const screen = render(
|
|
75
|
+
<TestWrapper>
|
|
76
|
+
<CopilotChatView autoScroll={true} messages={sampleMessages} />
|
|
77
|
+
</TestWrapper>,
|
|
78
|
+
);
|
|
79
|
+
await waitForMount(screen);
|
|
80
|
+
const spacer = screen.container.querySelector("[data-pin-to-send-spacer]");
|
|
81
|
+
expect(spacer).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("boolean false still maps to none (back-compat)", async () => {
|
|
85
|
+
const screen = render(
|
|
86
|
+
<TestWrapper>
|
|
87
|
+
<CopilotChatView autoScroll={false} messages={sampleMessages} />
|
|
88
|
+
</TestWrapper>,
|
|
89
|
+
);
|
|
90
|
+
await waitForMount(screen);
|
|
91
|
+
const spacer = screen.container.querySelector("[data-pin-to-send-spacer]");
|
|
92
|
+
expect(spacer).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -4,7 +4,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
4
4
|
import { CopilotChat } from "../CopilotChat";
|
|
5
5
|
import { useAgent } from "../../../hooks/use-agent";
|
|
6
6
|
import { useCopilotKit } from "../../../providers/CopilotKitProvider";
|
|
7
|
-
import { useCopilotChatConfiguration } from "../../../providers/CopilotChatConfigurationProvider";
|
|
8
7
|
import { MockStepwiseAgent } from "../../../__tests__/utils/test-helpers";
|
|
9
8
|
import { CopilotKitCoreRuntimeConnectionStatus } from "@copilotkit/core";
|
|
10
9
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
normalizeAutoScroll,
|
|
4
|
+
type AutoScrollMode,
|
|
5
|
+
} from "../normalize-auto-scroll";
|
|
6
|
+
|
|
7
|
+
describe("normalizeAutoScroll", () => {
|
|
8
|
+
it("returns 'pin-to-bottom' for undefined (default)", () => {
|
|
9
|
+
expect(normalizeAutoScroll(undefined)).toBe("pin-to-bottom");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("maps true -> 'pin-to-bottom'", () => {
|
|
13
|
+
expect(normalizeAutoScroll(true)).toBe("pin-to-bottom");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("maps false -> 'none'", () => {
|
|
17
|
+
expect(normalizeAutoScroll(false)).toBe("none");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("passes 'pin-to-bottom' through", () => {
|
|
21
|
+
expect(normalizeAutoScroll("pin-to-bottom")).toBe("pin-to-bottom");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("passes 'pin-to-send' through", () => {
|
|
25
|
+
expect(normalizeAutoScroll("pin-to-send")).toBe("pin-to-send");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("passes 'none' through", () => {
|
|
29
|
+
expect(normalizeAutoScroll("none")).toBe("none");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("falls back to 'pin-to-bottom' for unknown strings", () => {
|
|
33
|
+
expect(normalizeAutoScroll("bogus" as AutoScrollMode)).toBe(
|
|
34
|
+
"pin-to-bottom",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Context used by `CopilotChatView` to announce the latest user message
|
|
5
|
+
* to descendants (notably `usePinToSend`), so scroll logic can anchor
|
|
6
|
+
* the viewport to the most recent user turn in "pin-to-send" mode.
|
|
7
|
+
*
|
|
8
|
+
* `sendNonce` increments on each new send so repeated IDs (e.g., message
|
|
9
|
+
* edits that preserve the ID) still trigger dependent effects.
|
|
10
|
+
*/
|
|
11
|
+
export type LastUserMessageState = {
|
|
12
|
+
id: string | null;
|
|
13
|
+
sendNonce: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const LastUserMessageContext = React.createContext<LastUserMessageState>(
|
|
17
|
+
{
|
|
18
|
+
id: null,
|
|
19
|
+
sendNonce: 0,
|
|
20
|
+
},
|
|
21
|
+
);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type AutoScrollMode = "pin-to-bottom" | "pin-to-send" | "none";
|
|
2
|
+
|
|
3
|
+
const VALID: readonly AutoScrollMode[] = [
|
|
4
|
+
"pin-to-bottom",
|
|
5
|
+
"pin-to-send",
|
|
6
|
+
"none",
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export function normalizeAutoScroll(
|
|
10
|
+
value: AutoScrollMode | boolean | undefined,
|
|
11
|
+
): AutoScrollMode {
|
|
12
|
+
if (value === undefined) return "pin-to-bottom";
|
|
13
|
+
if (value === true) return "pin-to-bottom";
|
|
14
|
+
if (value === false) return "none";
|
|
15
|
+
if ((VALID as readonly string[]).includes(value)) return value;
|
|
16
|
+
return "pin-to-bottom";
|
|
17
|
+
}
|