@copilotkit/react-core 1.56.3 → 1.56.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copilotkit/react-core",
3
- "version": "1.56.3",
3
+ "version": "1.56.4",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "ai",
@@ -73,11 +73,11 @@
73
73
  "untruncate-json": "^0.0.1",
74
74
  "use-stick-to-bottom": "^1.1.1",
75
75
  "zod-to-json-schema": "^3.24.5",
76
- "@copilotkit/core": "1.56.3",
77
- "@copilotkit/a2ui-renderer": "1.56.3",
78
- "@copilotkit/runtime-client-gql": "1.56.3",
79
- "@copilotkit/web-inspector": "1.56.3",
80
- "@copilotkit/shared": "1.56.3"
76
+ "@copilotkit/a2ui-renderer": "1.56.4",
77
+ "@copilotkit/core": "1.56.4",
78
+ "@copilotkit/runtime-client-gql": "1.56.4",
79
+ "@copilotkit/web-inspector": "1.56.4",
80
+ "@copilotkit/shared": "1.56.4"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@tailwindcss/cli": "^4.1.11",
@@ -21,7 +21,10 @@ export const CopilotChatAttachmentQueue: React.FC<
21
21
  if (attachments.length === 0) return null;
22
22
 
23
23
  return (
24
- <div className={cn("cpk:flex cpk:flex-wrap cpk:gap-2 cpk:p-2", className)}>
24
+ <div
25
+ data-testid="copilot-attachment-queue"
26
+ className={cn("cpk:flex cpk:flex-wrap cpk:gap-2 cpk:p-2", className)}
27
+ >
25
28
  {attachments.map((attachment) => {
26
29
  const isMedia =
27
30
  attachment.type === "image" || attachment.type === "video";
@@ -6,17 +6,19 @@ import React, {
6
6
  useLayoutEffect,
7
7
  } from "react";
8
8
  import { ScrollElementContext } from "./scroll-element-context";
9
- import { WithSlots, SlotValue, renderSlot } from "../../lib/slots";
9
+ import type { WithSlots, SlotValue } from "../../lib/slots";
10
+ import { renderSlot } from "../../lib/slots";
10
11
  import CopilotChatMessageView from "./CopilotChatMessageView";
11
- import CopilotChatInput, {
12
+ import type {
12
13
  CopilotChatInputProps,
13
14
  CopilotChatInputMode,
14
15
  } from "./CopilotChatInput";
16
+ import CopilotChatInput from "./CopilotChatInput";
15
17
  import CopilotChatSuggestionView, {
16
18
  CopilotChatSuggestionViewProps,
17
19
  } from "./CopilotChatSuggestionView";
18
- import { Suggestion } from "@copilotkit/core";
19
- import { Message } from "@ag-ui/core";
20
+ import type { Suggestion } from "@copilotkit/core";
21
+ import type { Message } from "@ag-ui/core";
20
22
  import type { Attachment } from "@copilotkit/shared";
21
23
  import { CopilotChatAttachmentQueue } from "./CopilotChatAttachmentQueue";
22
24
  import { twMerge } from "tailwind-merge";
@@ -37,29 +39,8 @@ import { normalizeAutoScroll } from "./normalize-auto-scroll";
37
39
  import type { AutoScrollMode } from "./normalize-auto-scroll";
38
40
  import { usePinToSend } from "../../hooks/use-pin-to-send";
39
41
 
40
- // Height of the feather gradient overlay (h-24 = 6rem = 96px)
41
- const FEATHER_HEIGHT = 96;
42
-
43
- // Pin-to-send uses a softer, shorter feather than pin-to-bottom so readable
44
- // content isn't obscured (h-12 = 3rem = 48px).
45
- const PIN_TO_SEND_FEATHER_HEIGHT = 48;
46
-
47
- const PinToSendSoftFeather: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
48
- className,
49
- style,
50
- ...props
51
- }) => (
52
- <div
53
- className={cn(
54
- "cpk:absolute cpk:bottom-0 cpk:left-0 cpk:right-4 cpk:h-12 cpk:pointer-events-none cpk:z-10 cpk:bg-gradient-to-t",
55
- "cpk:from-white cpk:to-transparent",
56
- "cpk:dark:from-[rgb(33,33,33)]",
57
- className,
58
- )}
59
- style={style}
60
- {...props}
61
- />
62
- );
42
+ // Vertical gap between the scroll-to-bottom button and the input container.
43
+ const SCROLL_BUTTON_OFFSET = 16;
63
44
 
64
45
  // Forward declaration for WelcomeScreen component type
65
46
  export type WelcomeScreenProps = WithSlots<
@@ -258,11 +239,11 @@ export function CopilotChatView({
258
239
  onAddFile,
259
240
  positioning: "static",
260
241
  keyboardHeight: isKeyboardOpen ? keyboardHeight : 0,
261
- containerRef: inputContainerRef,
262
242
  showDisclaimer: true,
263
- // This input is the last flex child of the chat column, so it sits at
264
- // the bottom where the license banner would overlap. The welcome-screen
265
- // input (below) intentionally omits this flag.
243
+ // The parent overlay wrapper handles absolute bottom-0 positioning.
244
+ // `bottomAnchored` still triggers the license-banner offset padding
245
+ // inside CopilotChatInput. The welcome-screen input (below) intentionally
246
+ // omits this flag.
266
247
  bottomAnchored: true,
267
248
  ...(disclaimer !== undefined ? { disclaimer } : {}),
268
249
  } as CopilotChatInputProps);
@@ -291,8 +272,9 @@ export function CopilotChatView({
291
272
  isResizing,
292
273
  children: (
293
274
  <div
275
+ data-testid="copilot-scroll-content"
294
276
  style={{
295
- paddingBottom: `${hasSuggestions ? 4 : 32}px`,
277
+ paddingBottom: `${inputContainerHeight + (hasSuggestions ? 4 : 32)}px`,
296
278
  }}
297
279
  >
298
280
  <div className="cpk:max-w-3xl cpk:mx-auto">
@@ -415,17 +397,22 @@ export function CopilotChatView({
415
397
  {dragOver && <DropOverlay />}
416
398
  {BoundScrollView}
417
399
 
418
- <div className="cpk:max-w-3xl cpk:mx-auto cpk:w-full">
400
+ <div
401
+ ref={inputContainerRef}
402
+ data-testid="copilot-input-overlay"
403
+ className="cpk:absolute cpk:bottom-0 cpk:left-0 cpk:right-0 cpk:z-20 cpk:pointer-events-none"
404
+ >
419
405
  {attachments && attachments.length > 0 && (
420
- <CopilotChatAttachmentQueue
421
- attachments={attachments}
422
- onRemoveAttachment={(id) => onRemoveAttachment?.(id)}
423
- className="cpk:px-4"
424
- />
406
+ <div className="cpk:max-w-3xl cpk:mx-auto cpk:w-full cpk:pointer-events-auto">
407
+ <CopilotChatAttachmentQueue
408
+ attachments={attachments}
409
+ onRemoveAttachment={(id) => onRemoveAttachment?.(id)}
410
+ className="cpk:px-4"
411
+ />
412
+ </div>
425
413
  )}
414
+ {BoundInput}
426
415
  </div>
427
-
428
- {BoundInput}
429
416
  </div>
430
417
  );
431
418
  }
@@ -476,7 +463,6 @@ export namespace CopilotChatView {
476
463
  </div>
477
464
  </StickToBottom.Content>
478
465
 
479
- {/* Feather gradient overlay */}
480
466
  {BoundFeather}
481
467
 
482
468
  {/* Scroll to bottom button - hidden during resize */}
@@ -484,7 +470,7 @@ export namespace CopilotChatView {
484
470
  <div
485
471
  className="cpk:absolute cpk:inset-x-0 cpk:flex cpk:justify-center cpk:z-30 cpk:pointer-events-none"
486
472
  style={{
487
- bottom: `${inputContainerHeight + FEATHER_HEIGHT + 16}px`,
473
+ bottom: `${inputContainerHeight + SCROLL_BUTTON_OFFSET}px`,
488
474
  }}
489
475
  >
490
476
  {renderSlot(
@@ -541,21 +527,13 @@ export namespace CopilotChatView {
541
527
  topOffset: 16,
542
528
  });
543
529
 
544
- // Pin-to-send uses a SOFTER feather than pin-to-bottom:
545
- // - default: h-24 + from-white via-white to-transparent (fully opaque
546
- // bottom half, aggressive). Good for streaming-to-bottom where
547
- // the edge is always churning.
548
- // - pin-to-send: h-12 + from-white to-transparent (gradual fade,
549
- // no opaque midline). Gives a visual soft edge above the input
550
- // without obscuring otherwise-readable content.
551
- // Consumers can still override with the `feather` slot.
552
- const BoundFeather = renderSlot(feather, PinToSendSoftFeather, {});
553
-
554
- // Feather and scroll-to-bottom button live OUTSIDE the scroll container.
555
- // `position: absolute` children of an `overflow: auto` element are
556
- // positioned relative to the scroll *content*, which means they scroll
557
- // away with it. Placing them as siblings of the scroll container
530
+ // The feather and scroll-to-bottom button live OUTSIDE the scroll
531
+ // container. `position: absolute` children of an `overflow: auto` element
532
+ // are positioned relative to the scroll *content*, which means they
533
+ // scroll away with it. Placing them as siblings of the scroll container
558
534
  // (inside a `relative` wrapper) keeps them pinned to the viewport bottom.
535
+ const BoundFeather = renderSlot(feather, CopilotChatView.Feather, {});
536
+
559
537
  return (
560
538
  <ScrollElementContext.Provider value={nonAutoScrollEl}>
561
539
  <div
@@ -582,14 +560,13 @@ export namespace CopilotChatView {
582
560
  style={{ height: 0, flex: "0 0 auto" }}
583
561
  />
584
562
  </div>
585
- {/* Soft feather — pinned to wrapper bottom */}
586
563
  {BoundFeather}
587
564
  {/* Scroll to bottom button */}
588
565
  {showScrollButton && !isResizing && (
589
566
  <div
590
567
  className="cpk:absolute cpk:inset-x-0 cpk:flex cpk:justify-center cpk:z-30 cpk:pointer-events-none"
591
568
  style={{
592
- bottom: `${inputContainerHeight + PIN_TO_SEND_FEATHER_HEIGHT + 16}px`,
569
+ bottom: `${inputContainerHeight + SCROLL_BUTTON_OFFSET}px`,
593
570
  }}
594
571
  >
595
572
  {renderSlot(
@@ -722,7 +699,6 @@ export namespace CopilotChatView {
722
699
  {children}
723
700
  </div>
724
701
 
725
- {/* Feather gradient overlay */}
726
702
  {BoundFeather}
727
703
 
728
704
  {/* Scroll to bottom button for manual mode */}
@@ -730,7 +706,7 @@ export namespace CopilotChatView {
730
706
  <div
731
707
  className="cpk:absolute cpk:inset-x-0 cpk:flex cpk:justify-center cpk:z-30 cpk:pointer-events-none"
732
708
  style={{
733
- bottom: `${inputContainerHeight + FEATHER_HEIGHT + 16}px`,
709
+ bottom: `${inputContainerHeight + SCROLL_BUTTON_OFFSET}px`,
734
710
  }}
735
711
  >
736
712
  {renderSlot(
@@ -812,22 +788,14 @@ export namespace CopilotChatView {
812
788
  </Button>
813
789
  );
814
790
 
791
+ // Default renders an empty div — no visual, but the element is still in the
792
+ // tree so a slot override of the form `scrollView={{ feather: "my-class" }}`
793
+ // can apply classes (and any consumer with a full component override gets
794
+ // the className/style forwarding they expect).
815
795
  export const Feather: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
816
796
  className,
817
- style,
818
797
  ...props
819
- }) => (
820
- <div
821
- className={cn(
822
- "cpk:absolute cpk:bottom-0 cpk:left-0 cpk:right-4 cpk:h-24 cpk:pointer-events-none cpk:z-10 cpk:bg-gradient-to-t",
823
- "cpk:from-white cpk:via-white cpk:to-transparent",
824
- "cpk:dark:from-[rgb(33,33,33)] cpk:dark:via-[rgb(33,33,33)]",
825
- className,
826
- )}
827
- style={style}
828
- {...props}
829
- />
830
- );
798
+ }) => <div className={className} {...props} />;
831
799
 
832
800
  export const WelcomeMessage: React.FC<
833
801
  React.HTMLAttributes<HTMLDivElement>
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  import { fireEvent, render, screen, waitFor } from "@testing-library/react";
3
3
  import { z } from "zod";
4
+ import { EventType } from "@ag-ui/client";
4
5
  import {
5
6
  MockReconnectableAgent,
6
7
  MockStepwiseAgent,
@@ -10,34 +11,149 @@ import {
10
11
  runStartedEvent,
11
12
  testId,
12
13
  } from "../../../__tests__/utils/test-helpers";
13
- import { ReactActivityMessageRenderer } from "../../../types";
14
+ import type { ReactActivityMessageRenderer } from "../../../types";
14
15
  import {
15
16
  CopilotChatConfigurationProvider,
16
17
  CopilotKitProvider,
17
18
  useCopilotKit,
18
19
  } from "../../../providers";
19
- import { AbstractAgent } from "@ag-ui/client";
20
+ import type { AbstractAgent } from "@ag-ui/client";
20
21
  import { IntelligenceAgent } from "@copilotkit/core";
21
22
  import { getThreadClone } from "../../../hooks/use-agent";
22
23
  import { createA2UIMessageRenderer } from "../../../a2ui/A2UIMessageRenderer";
23
24
  import type { Theme } from "@copilotkit/a2ui-renderer";
24
25
  import { CopilotChat } from "..";
25
26
 
26
- const { mockWebsandboxCreate, mockWebsandboxDestroy } = vi.hoisted(() => {
27
+ const {
28
+ mockWebsandboxCreate,
29
+ mockWebsandboxDestroy,
30
+ mockPhoenixSockets,
31
+ MockPhoenixSocket,
32
+ } = vi.hoisted(() => {
27
33
  const mockDestroy = vi.fn();
28
- const mockCreate = vi.fn(() => ({
34
+ const mockCreate = vi.fn((..._args: unknown[]) => ({
29
35
  iframe: document.createElement("iframe"),
30
36
  promise: Promise.resolve(),
31
37
  run: vi.fn().mockResolvedValue(undefined),
32
38
  destroy: mockDestroy,
33
39
  }));
40
+ const mockSockets: MockPhoenixSocket[] = [];
41
+
42
+ class MockPhoenixPush {
43
+ private callbacks = new Map<string, (response?: unknown) => void>();
44
+
45
+ receive(
46
+ status: string,
47
+ callback: (response?: unknown) => void,
48
+ ): MockPhoenixPush {
49
+ this.callbacks.set(status, callback);
50
+ return this;
51
+ }
52
+
53
+ trigger(status: string, response?: unknown): void {
54
+ this.callbacks.get(status)?.(response);
55
+ }
56
+ }
57
+
58
+ class MockPhoenixChannel {
59
+ public topic: string;
60
+ public params: Record<string, unknown>;
61
+ public left = false;
62
+
63
+ private handlers = new Map<
64
+ string,
65
+ Array<{ ref: number; callback: (payload: unknown) => void }>
66
+ >();
67
+ private joinPush = new MockPhoenixPush();
68
+ private nextRef = 1;
69
+
70
+ constructor(topic: string, params: Record<string, unknown>) {
71
+ this.topic = topic;
72
+ this.params = params;
73
+ }
74
+
75
+ on(event: string, callback: (payload: unknown) => void): number {
76
+ if (!this.handlers.has(event)) {
77
+ this.handlers.set(event, []);
78
+ }
79
+ const ref = this.nextRef;
80
+ this.nextRef += 1;
81
+ this.handlers.get(event)?.push({ ref, callback });
82
+ return ref;
83
+ }
84
+
85
+ off(event: string, ref?: number): void {
86
+ if (ref === undefined) {
87
+ this.handlers.delete(event);
88
+ return;
89
+ }
90
+ this.handlers.set(
91
+ event,
92
+ (this.handlers.get(event) ?? []).filter(
93
+ (handler) => handler.ref !== ref,
94
+ ),
95
+ );
96
+ }
97
+
98
+ join(): MockPhoenixPush {
99
+ return this.joinPush;
100
+ }
101
+
102
+ leave(): void {
103
+ this.left = true;
104
+ }
105
+
106
+ triggerJoin(status: string, response?: unknown): void {
107
+ this.joinPush.trigger(status, response);
108
+ }
109
+
110
+ serverPush(event: string, payload: unknown): void {
111
+ for (const { callback } of this.handlers.get(event) ?? []) {
112
+ callback(payload);
113
+ }
114
+ }
115
+ }
116
+
117
+ class MockPhoenixSocket {
118
+ public channels: MockPhoenixChannel[] = [];
119
+
120
+ constructor(
121
+ public url: string,
122
+ public opts: Record<string, unknown>,
123
+ ) {
124
+ mockSockets.push(this);
125
+ }
126
+
127
+ connect(): void {}
128
+
129
+ disconnect(): void {}
130
+
131
+ onOpen(): void {}
132
+
133
+ onError(): void {}
134
+
135
+ channel(
136
+ topic: string,
137
+ params: Record<string, unknown>,
138
+ ): MockPhoenixChannel {
139
+ const channel = new MockPhoenixChannel(topic, params);
140
+ this.channels.push(channel);
141
+ return channel;
142
+ }
143
+ }
34
144
 
35
145
  return {
36
146
  mockWebsandboxCreate: mockCreate,
37
147
  mockWebsandboxDestroy: mockDestroy,
148
+ mockPhoenixSockets: mockSockets,
149
+ MockPhoenixSocket,
38
150
  };
39
151
  });
40
152
 
153
+ vi.mock("phoenix", () => ({
154
+ Socket: MockPhoenixSocket,
155
+ }));
156
+
41
157
  vi.mock("@jetbrains/websandbox", () => ({
42
158
  default: {
43
159
  create: (...args: unknown[]) => mockWebsandboxCreate(...args),
@@ -329,66 +445,22 @@ describe("CopilotChat activity message rendering", () => {
329
445
  });
330
446
  });
331
447
 
332
- it("restores a completed A2UI surface from an IntelligenceAgent /connect bootstrap plan", async () => {
448
+ it("restores a completed A2UI surface from IntelligenceAgent /connect gateway replay", async () => {
333
449
  const threadId = testId("intelligence-connect-thread");
334
450
  const surfaceId = testId("intelligence-connect-surface");
335
451
  const fetchMock = vi.fn().mockResolvedValueOnce(
336
452
  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
- ],
453
+ threadId,
454
+ runId: null,
455
+ joinToken: "join-token-1",
456
+ realtime: {
457
+ clientUrl: "ws://localhost:4000/client",
458
+ topic: `thread:${threadId}`,
459
+ },
389
460
  }),
390
461
  );
391
462
  vi.stubGlobal("fetch", fetchMock);
463
+ mockPhoenixSockets.length = 0;
392
464
 
393
465
  const agent = new IntelligenceAgent({
394
466
  url: "ws://localhost:4000/client",
@@ -409,6 +481,70 @@ describe("CopilotChat activity message rendering", () => {
409
481
  await waitFor(() => {
410
482
  expect(fetchMock).toHaveBeenCalledTimes(1);
411
483
  });
484
+
485
+ await waitFor(() => {
486
+ expect(mockPhoenixSockets).toHaveLength(1);
487
+ expect(mockPhoenixSockets[0]?.channels).toHaveLength(1);
488
+ });
489
+
490
+ const channel = mockPhoenixSockets[0]!.channels[0]!;
491
+ expect(channel.topic).toBe(`thread:${threadId}`);
492
+ expect(channel.params).toEqual({
493
+ stream_mode: "connect",
494
+ last_seen_event_id: null,
495
+ });
496
+
497
+ channel.triggerJoin("ok");
498
+ channel.serverPush("ag_ui_event", {
499
+ type: EventType.RUN_STARTED,
500
+ threadId,
501
+ run_id: "backend-run-1",
502
+ input: {
503
+ messages: [
504
+ {
505
+ id: testId("connect-user-message"),
506
+ role: "user",
507
+ content: "show me the restored ui",
508
+ },
509
+ ],
510
+ },
511
+ });
512
+ channel.serverPush("ag_ui_event", {
513
+ type: EventType.ACTIVITY_SNAPSHOT,
514
+ messageId: testId("connect-a2ui-activity"),
515
+ activityType: "a2ui-surface",
516
+ content: {
517
+ a2ui_operations: [
518
+ {
519
+ version: "v0.9",
520
+ createSurface: {
521
+ surfaceId,
522
+ catalogId:
523
+ "https://a2ui.org/specification/v0_9/basic_catalog.json",
524
+ },
525
+ },
526
+ {
527
+ version: "v0.9",
528
+ updateComponents: {
529
+ surfaceId,
530
+ components: [
531
+ {
532
+ id: "root",
533
+ component: "Text",
534
+ text: "Restored dashboard",
535
+ variant: "body",
536
+ },
537
+ ],
538
+ },
539
+ },
540
+ ],
541
+ },
542
+ });
543
+ channel.serverPush("ag_ui_event", {
544
+ type: EventType.RUN_FINISHED,
545
+ });
546
+ channel.serverPush("stream_idle", { latestEventId: "event-3" });
547
+
412
548
  await waitFor(() => {
413
549
  expect(screen.getByText("show me the restored ui")).toBeDefined();
414
550
  });