@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/dist/{copilotkit-PzJlPKcU.mjs → copilotkit-Bd0m5HFp.mjs} +24 -26
- package/dist/{copilotkit-PzJlPKcU.mjs.map → copilotkit-Bd0m5HFp.mjs.map} +1 -1
- package/dist/copilotkit-DFaI4j2r.d.mts.map +1 -1
- package/dist/copilotkit-Dg4r4Gi_.d.cts.map +1 -1
- package/dist/{copilotkit-By2G6-Zx.cjs → copilotkit-tb4zqaMK.cjs} +24 -26
- package/dist/{copilotkit-By2G6-Zx.cjs.map → copilotkit-tb4zqaMK.cjs.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +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 +23 -25
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +6 -6
- package/src/v2/components/chat/CopilotChatAttachmentQueue.tsx +4 -1
- package/src/v2/components/chat/CopilotChatView.tsx +41 -73
- package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +193 -57
- package/src/v2/components/chat/__tests__/CopilotChatView.inputOverlay.test.tsx +172 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@copilotkit/react-core",
|
|
3
|
-
"version": "1.56.
|
|
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/
|
|
77
|
-
"@copilotkit/
|
|
78
|
-
"@copilotkit/runtime-client-gql": "1.56.
|
|
79
|
-
"@copilotkit/web-inspector": "1.56.
|
|
80
|
-
"@copilotkit/shared": "1.56.
|
|
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
|
|
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
|
|
9
|
+
import type { WithSlots, SlotValue } from "../../lib/slots";
|
|
10
|
+
import { renderSlot } from "../../lib/slots";
|
|
10
11
|
import CopilotChatMessageView from "./CopilotChatMessageView";
|
|
11
|
-
import
|
|
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
|
-
//
|
|
41
|
-
const
|
|
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
|
-
//
|
|
264
|
-
//
|
|
265
|
-
// input (below) intentionally
|
|
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
|
|
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
|
-
<
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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 +
|
|
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
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
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 +
|
|
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 +
|
|
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 {
|
|
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
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
});
|