@copilotkit/react-core 1.56.4-canary.1777629975 → 1.56.4-canary.1777659843
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-tb4zqaMK.cjs → copilotkit-Bn7CCpNA.cjs} +12 -36
- package/dist/{copilotkit-tb4zqaMK.cjs.map → copilotkit-Bn7CCpNA.cjs.map} +1 -1
- package/dist/copilotkit-DFaI4j2r.d.mts.map +1 -1
- package/dist/{copilotkit-Bd0m5HFp.mjs → copilotkit-DHEUlli6.mjs} +12 -36
- package/dist/{copilotkit-Bd0m5HFp.mjs.map → copilotkit-DHEUlli6.mjs.map} +1 -1
- package/dist/copilotkit-Dg4r4Gi_.d.cts.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.mjs +1 -1
- package/dist/v2/index.umd.js +11 -35
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +6 -6
- package/src/v2/components/chat/CopilotChatView.tsx +1 -9
- package/src/v2/components/chat/__tests__/CopilotChatView.pinToSend.test.tsx +23 -13
- package/src/v2/hooks/__tests__/use-pin-to-send.test.tsx +48 -53
- package/src/v2/hooks/use-pin-to-send.ts +23 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@copilotkit/react-core",
|
|
3
|
-
"version": "1.56.4-canary.
|
|
3
|
+
"version": "1.56.4-canary.1777659843",
|
|
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/
|
|
79
|
-
"@copilotkit/shared": "1.56.4-canary.
|
|
80
|
-
"@copilotkit/
|
|
76
|
+
"@copilotkit/core": "1.56.4-canary.1777659843",
|
|
77
|
+
"@copilotkit/a2ui-renderer": "1.56.4-canary.1777659843",
|
|
78
|
+
"@copilotkit/runtime-client-gql": "1.56.4-canary.1777659843",
|
|
79
|
+
"@copilotkit/shared": "1.56.4-canary.1777659843",
|
|
80
|
+
"@copilotkit/web-inspector": "1.56.4-canary.1777659843"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"@tailwindcss/cli": "^4.1.11",
|
|
@@ -518,12 +518,9 @@ export namespace CopilotChatView {
|
|
|
518
518
|
className,
|
|
519
519
|
...props
|
|
520
520
|
}) => {
|
|
521
|
-
const spacerRef = useRef<HTMLDivElement>(null);
|
|
522
|
-
|
|
523
521
|
usePinToSend({
|
|
524
522
|
scrollRef,
|
|
525
523
|
contentRef,
|
|
526
|
-
spacerRef,
|
|
527
524
|
topOffset: 16,
|
|
528
525
|
});
|
|
529
526
|
|
|
@@ -549,16 +546,11 @@ export namespace CopilotChatView {
|
|
|
549
546
|
>
|
|
550
547
|
<div
|
|
551
548
|
ref={contentRef}
|
|
549
|
+
data-pin-to-send-content
|
|
552
550
|
className="cpk:px-4 cpk:sm:px-0 cpk:[div[data-sidebar-chat]_&]:px-8 cpk:[div[data-popup-chat]_&]:px-6"
|
|
553
551
|
>
|
|
554
552
|
{children}
|
|
555
553
|
</div>
|
|
556
|
-
<div
|
|
557
|
-
ref={spacerRef}
|
|
558
|
-
data-pin-to-send-spacer
|
|
559
|
-
aria-hidden="true"
|
|
560
|
-
style={{ height: 0, flex: "0 0 auto" }}
|
|
561
|
-
/>
|
|
562
554
|
</div>
|
|
563
555
|
{BoundFeather}
|
|
564
556
|
{/* Scroll to bottom button */}
|
|
@@ -35,7 +35,7 @@ async function waitForMount(screen: {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
describe("CopilotChatView pin-to-send mode", () => {
|
|
38
|
-
it("renders the pin-to-send
|
|
38
|
+
it("renders the pin-to-send content container when autoScroll='pin-to-send'", async () => {
|
|
39
39
|
const screen = render(
|
|
40
40
|
<TestWrapper>
|
|
41
41
|
<LastUserMessageContext.Provider value={{ id: null, sendNonce: 0 }}>
|
|
@@ -44,30 +44,36 @@ describe("CopilotChatView pin-to-send mode", () => {
|
|
|
44
44
|
</TestWrapper>,
|
|
45
45
|
);
|
|
46
46
|
await waitForMount(screen);
|
|
47
|
-
const
|
|
48
|
-
|
|
47
|
+
const content = screen.container.querySelector(
|
|
48
|
+
"[data-pin-to-send-content]",
|
|
49
|
+
);
|
|
50
|
+
expect(content).not.toBeNull();
|
|
49
51
|
});
|
|
50
52
|
|
|
51
|
-
it("does not render the
|
|
53
|
+
it("does not render the pin-to-send content marker when autoScroll='pin-to-bottom'", async () => {
|
|
52
54
|
const screen = render(
|
|
53
55
|
<TestWrapper>
|
|
54
56
|
<CopilotChatView autoScroll="pin-to-bottom" messages={sampleMessages} />
|
|
55
57
|
</TestWrapper>,
|
|
56
58
|
);
|
|
57
59
|
await waitForMount(screen);
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
+
const content = screen.container.querySelector(
|
|
61
|
+
"[data-pin-to-send-content]",
|
|
62
|
+
);
|
|
63
|
+
expect(content).toBeNull();
|
|
60
64
|
});
|
|
61
65
|
|
|
62
|
-
it("does not render the
|
|
66
|
+
it("does not render the pin-to-send content marker when autoScroll='none'", async () => {
|
|
63
67
|
const screen = render(
|
|
64
68
|
<TestWrapper>
|
|
65
69
|
<CopilotChatView autoScroll="none" messages={sampleMessages} />
|
|
66
70
|
</TestWrapper>,
|
|
67
71
|
);
|
|
68
72
|
await waitForMount(screen);
|
|
69
|
-
const
|
|
70
|
-
|
|
73
|
+
const content = screen.container.querySelector(
|
|
74
|
+
"[data-pin-to-send-content]",
|
|
75
|
+
);
|
|
76
|
+
expect(content).toBeNull();
|
|
71
77
|
});
|
|
72
78
|
|
|
73
79
|
it("boolean true still maps to pin-to-bottom (back-compat)", async () => {
|
|
@@ -77,8 +83,10 @@ describe("CopilotChatView pin-to-send mode", () => {
|
|
|
77
83
|
</TestWrapper>,
|
|
78
84
|
);
|
|
79
85
|
await waitForMount(screen);
|
|
80
|
-
const
|
|
81
|
-
|
|
86
|
+
const content = screen.container.querySelector(
|
|
87
|
+
"[data-pin-to-send-content]",
|
|
88
|
+
);
|
|
89
|
+
expect(content).toBeNull();
|
|
82
90
|
});
|
|
83
91
|
|
|
84
92
|
it("boolean false still maps to none (back-compat)", async () => {
|
|
@@ -88,7 +96,9 @@ describe("CopilotChatView pin-to-send mode", () => {
|
|
|
88
96
|
</TestWrapper>,
|
|
89
97
|
);
|
|
90
98
|
await waitForMount(screen);
|
|
91
|
-
const
|
|
92
|
-
|
|
99
|
+
const content = screen.container.querySelector(
|
|
100
|
+
"[data-pin-to-send-content]",
|
|
101
|
+
);
|
|
102
|
+
expect(content).toBeNull();
|
|
93
103
|
});
|
|
94
104
|
});
|
|
@@ -33,9 +33,8 @@ function setHeight(el: HTMLElement, height: number) {
|
|
|
33
33
|
function HarnessInner({ topOffset }: { topOffset: number }) {
|
|
34
34
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
35
35
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
36
|
-
const spacerRef = useRef<HTMLDivElement>(null);
|
|
37
36
|
|
|
38
|
-
usePinToSend({ scrollRef, contentRef,
|
|
37
|
+
usePinToSend({ scrollRef, contentRef, topOffset });
|
|
39
38
|
|
|
40
39
|
return (
|
|
41
40
|
<div ref={scrollRef} data-testid="scroll">
|
|
@@ -50,7 +49,6 @@ function HarnessInner({ topOffset }: { topOffset: number }) {
|
|
|
50
49
|
user msg 2
|
|
51
50
|
</div>
|
|
52
51
|
</div>
|
|
53
|
-
<div ref={spacerRef} data-testid="spacer" style={{ height: 0 }} />
|
|
54
52
|
</div>
|
|
55
53
|
);
|
|
56
54
|
}
|
|
@@ -80,30 +78,42 @@ beforeEach(() => {
|
|
|
80
78
|
});
|
|
81
79
|
|
|
82
80
|
describe("usePinToSend", () => {
|
|
83
|
-
it("sets
|
|
81
|
+
it("sets content min-height to userMsgPos + viewportHeight - topOffset on new send", async () => {
|
|
84
82
|
const { rerender, getByTestId } = render(
|
|
85
83
|
<Harness lastUserMessage={{ id: null, sendNonce: 0 }} />,
|
|
86
84
|
);
|
|
87
85
|
|
|
88
86
|
const scroll = getByTestId("scroll");
|
|
89
|
-
const
|
|
87
|
+
const content = getByTestId("content");
|
|
90
88
|
setHeight(scroll, 800);
|
|
91
89
|
|
|
92
90
|
const userMsg = scroll.querySelector(
|
|
93
91
|
'[data-message-id="m3"]',
|
|
94
92
|
) as HTMLElement;
|
|
95
|
-
|
|
93
|
+
// Mock user msg at scroll offset 400, height 40, no top padding.
|
|
94
|
+
userMsg.getBoundingClientRect = () =>
|
|
95
|
+
({
|
|
96
|
+
top: 400,
|
|
97
|
+
left: 0,
|
|
98
|
+
right: 100,
|
|
99
|
+
bottom: 440,
|
|
100
|
+
width: 100,
|
|
101
|
+
height: 40,
|
|
102
|
+
x: 0,
|
|
103
|
+
y: 400,
|
|
104
|
+
toJSON: () => ({}),
|
|
105
|
+
}) as DOMRect;
|
|
96
106
|
|
|
97
107
|
act(() => {
|
|
98
108
|
rerender(<Harness lastUserMessage={{ id: "m3", sendNonce: 1 }} />);
|
|
99
109
|
});
|
|
100
110
|
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
expect(
|
|
111
|
+
// userMsgPos=400, paddingTop=0, viewport=800, topOffset=16
|
|
112
|
+
// minHeight = 400 + 0 + 800 - 16 = 1184
|
|
113
|
+
expect(content.style.minHeight).toBe("1184px");
|
|
104
114
|
});
|
|
105
115
|
|
|
106
|
-
it("calls scrollTo with
|
|
116
|
+
it("calls scrollTo with userMsgPos + paddingTop - topOffset on new send", async () => {
|
|
107
117
|
const { rerender, getByTestId } = render(
|
|
108
118
|
<Harness lastUserMessage={{ id: null, sendNonce: 0 }} />,
|
|
109
119
|
);
|
|
@@ -115,7 +125,6 @@ describe("usePinToSend", () => {
|
|
|
115
125
|
const userMsg = scroll.querySelector(
|
|
116
126
|
'[data-message-id="m3"]',
|
|
117
127
|
) as HTMLElement;
|
|
118
|
-
setHeight(userMsg, 40);
|
|
119
128
|
// computeOffsetTop uses getBoundingClientRect; mock top=400 on userMsg and top=0 on scroll
|
|
120
129
|
// so that elRect.top - stopRect.top + scrollEl.scrollTop = 400 - 0 + 0 = 400.
|
|
121
130
|
userMsg.getBoundingClientRect = () =>
|
|
@@ -146,50 +155,36 @@ describe("usePinToSend", () => {
|
|
|
146
155
|
});
|
|
147
156
|
});
|
|
148
157
|
|
|
149
|
-
it("
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
setHeight(content, 200);
|
|
172
|
-
|
|
173
|
-
act(() => {
|
|
174
|
-
rerender(<Harness lastUserMessage={{ id: "m3", sendNonce: 1 }} />);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// Initial: 800 - 40 - 16 = 744
|
|
178
|
-
expect(spacer.style.height).toBe("744px");
|
|
158
|
+
it("clears content min-height on cleanup so the next send resets the floor", async () => {
|
|
159
|
+
const { rerender, unmount, getByTestId } = render(
|
|
160
|
+
<Harness lastUserMessage={{ id: null, sendNonce: 0 }} />,
|
|
161
|
+
);
|
|
162
|
+
const scroll = getByTestId("scroll");
|
|
163
|
+
const content = getByTestId("content");
|
|
164
|
+
setHeight(scroll, 800);
|
|
165
|
+
const userMsg = scroll.querySelector(
|
|
166
|
+
'[data-message-id="m3"]',
|
|
167
|
+
) as HTMLElement;
|
|
168
|
+
userMsg.getBoundingClientRect = () =>
|
|
169
|
+
({
|
|
170
|
+
top: 400,
|
|
171
|
+
left: 0,
|
|
172
|
+
right: 100,
|
|
173
|
+
bottom: 440,
|
|
174
|
+
width: 100,
|
|
175
|
+
height: 40,
|
|
176
|
+
x: 0,
|
|
177
|
+
y: 400,
|
|
178
|
+
toJSON: () => ({}),
|
|
179
|
+
}) as DOMRect;
|
|
179
180
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
act(() => {
|
|
182
|
+
rerender(<Harness lastUserMessage={{ id: "m3", sendNonce: 1 }} />);
|
|
183
|
+
});
|
|
184
|
+
expect(content.style.minHeight).toBe("1184px");
|
|
184
185
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const shrunkHeight = spacer.style.height;
|
|
188
|
-
act(() => observed?.());
|
|
189
|
-
expect(spacer.style.height).toBe(shrunkHeight);
|
|
190
|
-
} finally {
|
|
191
|
-
global.ResizeObserver = prevRO;
|
|
192
|
-
}
|
|
186
|
+
unmount();
|
|
187
|
+
expect(content.style.minHeight).toBe("");
|
|
193
188
|
});
|
|
194
189
|
|
|
195
190
|
it("cancels the scheduled rAF on unmount (cleanup)", async () => {
|
|
@@ -4,19 +4,22 @@ import { LastUserMessageContext } from "../components/chat/last-user-message-con
|
|
|
4
4
|
export type UsePinToSendOptions = {
|
|
5
5
|
scrollRef: React.RefObject<HTMLElement | null>;
|
|
6
6
|
contentRef: React.RefObject<HTMLElement | null>;
|
|
7
|
-
spacerRef: React.RefObject<HTMLElement | null>;
|
|
8
7
|
topOffset?: number;
|
|
9
8
|
};
|
|
10
9
|
|
|
10
|
+
// Apply a transient min-height to the scroll content so the newly sent user
|
|
11
|
+
// message can scroll to `topOffset` from the viewport top, then let natural
|
|
12
|
+
// content (the streaming assistant response, suggestions, etc.) take over as
|
|
13
|
+
// it grows past that floor. This mirrors the ChatGPT / Claude behavior:
|
|
14
|
+
// user message pinned to top, response streams in below, no extra scrollable
|
|
15
|
+
// whitespace tacked on after generation.
|
|
11
16
|
export function usePinToSend({
|
|
12
17
|
scrollRef,
|
|
13
18
|
contentRef,
|
|
14
|
-
spacerRef,
|
|
15
19
|
topOffset = 16,
|
|
16
20
|
}: UsePinToSendOptions): void {
|
|
17
21
|
const { id, sendNonce } = useContext(LastUserMessageContext);
|
|
18
22
|
const lastNonceRef = useRef<number>(-1);
|
|
19
|
-
const currentSpacerHeightRef = useRef<number>(0);
|
|
20
23
|
|
|
21
24
|
useEffect(() => {
|
|
22
25
|
if (sendNonce === lastNonceRef.current) return;
|
|
@@ -25,8 +28,7 @@ export function usePinToSend({
|
|
|
25
28
|
if (!id) return;
|
|
26
29
|
const scrollEl = scrollRef.current;
|
|
27
30
|
const contentEl = contentRef.current;
|
|
28
|
-
|
|
29
|
-
if (!scrollEl || !contentEl || !spacerEl) return;
|
|
31
|
+
if (!scrollEl || !contentEl) return;
|
|
30
32
|
|
|
31
33
|
const escaped =
|
|
32
34
|
typeof CSS !== "undefined" && CSS.escape
|
|
@@ -44,45 +46,34 @@ export function usePinToSend({
|
|
|
44
46
|
// whatever was above the element too — including the previous message's
|
|
45
47
|
// trailing copy button).
|
|
46
48
|
const viewportHeight = scrollEl.clientHeight;
|
|
47
|
-
const userMessageHeight = targetEl.getBoundingClientRect().height;
|
|
48
49
|
const paddingTop = parseFloat(getComputedStyle(targetEl).paddingTop) || 0;
|
|
49
|
-
const
|
|
50
|
-
const spacerHeight = Math.max(0, viewportHeight - bubbleHeight - topOffset);
|
|
50
|
+
const userMsgOffsetInScroll = computeOffsetTop(targetEl, scrollEl);
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
// The minimum scrollHeight that lets the bubble land at `topOffset` from
|
|
53
|
+
// the viewport top is: `bubbleTop + viewportHeight - topOffset`, where
|
|
54
|
+
// `bubbleTop = userMsgOffsetInScroll + paddingTop`. We apply this as a
|
|
55
|
+
// min-height on the content element. Once the natural content (response,
|
|
56
|
+
// padding, suggestions) grows past this floor, the floor is irrelevant
|
|
57
|
+
// and the layout reflects only the actual content — no leftover spacer
|
|
58
|
+
// whitespace at the bottom after generation.
|
|
59
|
+
const requiredScrollHeight =
|
|
60
|
+
userMsgOffsetInScroll + paddingTop + viewportHeight - topOffset;
|
|
61
|
+
contentEl.style.minHeight = `${Math.max(0, requiredScrollHeight)}px`;
|
|
54
62
|
|
|
55
63
|
const raf = requestAnimationFrame(() => {
|
|
56
64
|
// Scroll so the BUBBLE is `topOffset` from the viewport top — the
|
|
57
65
|
// padding above the bubble ends up scrolled off-screen.
|
|
58
|
-
const targetTop =
|
|
59
|
-
computeOffsetTop(targetEl, scrollEl) + paddingTop - topOffset;
|
|
66
|
+
const targetTop = userMsgOffsetInScroll + paddingTop - topOffset;
|
|
60
67
|
scrollEl.scrollTo({ top: Math.max(0, targetTop), behavior: "smooth" });
|
|
61
68
|
});
|
|
62
69
|
|
|
63
|
-
// Shrink-only ResizeObserver: as the assistant response grows below the
|
|
64
|
-
// anchored user message, collapse the spacer by the same amount so total
|
|
65
|
-
// scrollable space below the bubble stays constant (and the bubble stays
|
|
66
|
-
// pinned). Never grow the spacer after initial sizing.
|
|
67
|
-
const ro = new ResizeObserver(() => {
|
|
68
|
-
if (!contentEl || !spacerEl || !scrollEl) return;
|
|
69
|
-
const contentHeight = contentEl.getBoundingClientRect().height;
|
|
70
|
-
const targetOffsetWithinContent = computeOffsetTop(targetEl, contentEl);
|
|
71
|
-
const consumedBelow =
|
|
72
|
-
contentHeight - targetOffsetWithinContent - userMessageHeight;
|
|
73
|
-
const remaining = Math.max(0, spacerHeight - consumedBelow);
|
|
74
|
-
if (remaining < currentSpacerHeightRef.current) {
|
|
75
|
-
spacerEl.style.height = `${remaining}px`;
|
|
76
|
-
currentSpacerHeightRef.current = remaining;
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
ro.observe(contentEl);
|
|
80
|
-
|
|
81
70
|
return () => {
|
|
82
71
|
cancelAnimationFrame(raf);
|
|
83
|
-
|
|
72
|
+
// Clear the floor on cleanup so a subsequent send (with a different
|
|
73
|
+
// user-message position) starts from a clean slate.
|
|
74
|
+
contentEl.style.minHeight = "";
|
|
84
75
|
};
|
|
85
|
-
}, [id, sendNonce, scrollRef, contentRef,
|
|
76
|
+
}, [id, sendNonce, scrollRef, contentRef, topOffset]);
|
|
86
77
|
}
|
|
87
78
|
|
|
88
79
|
// Compute the offset of el relative to stopAt, accounting for stopAt's current scrollTop.
|