@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@copilotkit/react-core",
|
|
3
|
-
"version": "1.56.
|
|
3
|
+
"version": "1.56.2-canary.pin-to-send",
|
|
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/
|
|
76
|
+
"@copilotkit/core": "1.56.2-canary.pin-to-send",
|
|
77
|
+
"@copilotkit/shared": "1.56.2-canary.pin-to-send",
|
|
78
|
+
"@copilotkit/runtime-client-gql": "1.56.2-canary.pin-to-send",
|
|
79
|
+
"@copilotkit/web-inspector": "1.56.2-canary.pin-to-send",
|
|
80
|
+
"@copilotkit/a2ui-renderer": "1.56.2-canary.pin-to-send"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"@tailwindcss/cli": "^4.1.11",
|
|
@@ -60,16 +60,27 @@ const usePredictStateSubscription = (agent?: AbstractAgent) => {
|
|
|
60
60
|
}, [agent, getSubscriber]);
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
const { copilotkit } = useCopilotKit();
|
|
63
|
+
function CopilotListenersAgentSubscription() {
|
|
65
64
|
const existingConfig = useCopilotChatConfiguration();
|
|
66
65
|
const resolvedAgentId = existingConfig?.agentId;
|
|
67
|
-
const { setBannerError } = useToast();
|
|
68
66
|
|
|
69
67
|
const { agent } = useAgent({ agentId: resolvedAgentId });
|
|
70
68
|
|
|
71
69
|
usePredictStateSubscription(agent);
|
|
72
70
|
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function CopilotListeners() {
|
|
75
|
+
const { copilotkit } = useCopilotKit();
|
|
76
|
+
const { setBannerError } = useToast();
|
|
77
|
+
|
|
78
|
+
// Only render the agent subscription when agents are registered or a runtime
|
|
79
|
+
// is configured. Without this guard, useAgent() throws when the agents map is
|
|
80
|
+
// empty and no runtimeUrl is set (#3249).
|
|
81
|
+
const hasAgents = Object.keys(copilotkit.agents ?? {}).length > 0;
|
|
82
|
+
const hasRuntime = copilotkit.runtimeUrl !== undefined;
|
|
83
|
+
|
|
73
84
|
useEffect(() => {
|
|
74
85
|
const subscriber: CopilotKitCoreSubscriber = {
|
|
75
86
|
onError: ({ error, code, context }) => {
|
|
@@ -122,5 +133,5 @@ export function CopilotListeners() {
|
|
|
122
133
|
};
|
|
123
134
|
}, [copilotkit?.subscribe]);
|
|
124
135
|
|
|
125
|
-
return null;
|
|
136
|
+
return hasAgents || hasRuntime ? <CopilotListenersAgentSubscription /> : null;
|
|
126
137
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
3
|
+
import { render } from "@testing-library/react";
|
|
4
|
+
import { CopilotListeners } from "../CopilotListeners";
|
|
5
|
+
import { CopilotKitProvider } from "../../v2/providers/CopilotKitProvider";
|
|
6
|
+
import { CopilotChatConfigurationProvider } from "../../v2/providers/CopilotChatConfigurationProvider";
|
|
7
|
+
import { ToastProvider } from "../toast/toast-provider";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Regression test for #3249: CopilotListeners throws when no agents registered.
|
|
11
|
+
*
|
|
12
|
+
* When CopilotKitProvider has no agents registered (empty agents map) and no
|
|
13
|
+
* runtimeUrl, useAgent() inside CopilotListeners throws. The component should
|
|
14
|
+
* handle this gracefully and render null without crashing.
|
|
15
|
+
*/
|
|
16
|
+
describe("CopilotListeners (#3249)", () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("does not throw when no agents are registered", () => {
|
|
22
|
+
// No agents, no runtimeUrl - should not crash
|
|
23
|
+
expect(() => {
|
|
24
|
+
render(
|
|
25
|
+
<ToastProvider enabled={false}>
|
|
26
|
+
<CopilotKitProvider>
|
|
27
|
+
<CopilotChatConfigurationProvider
|
|
28
|
+
agentId="default"
|
|
29
|
+
threadId="test-thread"
|
|
30
|
+
>
|
|
31
|
+
<CopilotListeners />
|
|
32
|
+
</CopilotChatConfigurationProvider>
|
|
33
|
+
</CopilotKitProvider>
|
|
34
|
+
</ToastProvider>,
|
|
35
|
+
);
|
|
36
|
+
}).not.toThrow();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -33,6 +33,10 @@ import {
|
|
|
33
33
|
transcribeAudio,
|
|
34
34
|
TranscriptionError,
|
|
35
35
|
} from "../../lib/transcription-client";
|
|
36
|
+
import {
|
|
37
|
+
LastUserMessageContext,
|
|
38
|
+
type LastUserMessageState,
|
|
39
|
+
} from "./last-user-message-context";
|
|
36
40
|
|
|
37
41
|
export type CopilotChatProps = Omit<
|
|
38
42
|
CopilotChatViewProps,
|
|
@@ -97,9 +101,10 @@ export function CopilotChat({
|
|
|
97
101
|
// Apply priority: props > existing config > defaults
|
|
98
102
|
const resolvedAgentId =
|
|
99
103
|
agentId ?? existingConfig?.agentId ?? DEFAULT_AGENT_ID;
|
|
104
|
+
const providedThreadId = threadId ?? existingConfig?.threadId;
|
|
100
105
|
const resolvedThreadId = useMemo(
|
|
101
|
-
() =>
|
|
102
|
-
[
|
|
106
|
+
() => providedThreadId ?? randomUUID(),
|
|
107
|
+
[providedThreadId],
|
|
103
108
|
);
|
|
104
109
|
|
|
105
110
|
const { agent } = useAgent({
|
|
@@ -191,7 +196,24 @@ export function CopilotChat({
|
|
|
191
196
|
...restProps
|
|
192
197
|
} = props;
|
|
193
198
|
|
|
199
|
+
// Tracks the last threadId for which connectAgent has completed (success or
|
|
200
|
+
// failure). When the user supplies a threadId, we're in "resume existing
|
|
201
|
+
// thread" mode — the welcome screen should be suppressed until the connect
|
|
202
|
+
// resolves, otherwise switching threads flashes the welcome screen while the
|
|
203
|
+
// new thread's messages are still en route.
|
|
204
|
+
const [lastConnectedThreadId, setLastConnectedThreadId] = useState<
|
|
205
|
+
string | null
|
|
206
|
+
>(null);
|
|
207
|
+
const isConnecting =
|
|
208
|
+
!!providedThreadId && lastConnectedThreadId !== resolvedThreadId;
|
|
209
|
+
|
|
194
210
|
useEffect(() => {
|
|
211
|
+
// When no threadId was supplied by the caller, resolvedThreadId is a UUID
|
|
212
|
+
// minted in this browser tab. The backend has never seen it, so /connect
|
|
213
|
+
// would always 404. Skip the call — a real thread is only created once
|
|
214
|
+
// the user runs the agent for the first time.
|
|
215
|
+
if (!providedThreadId) return;
|
|
216
|
+
|
|
195
217
|
let detached = false;
|
|
196
218
|
|
|
197
219
|
// Create a fresh AbortController so we can cancel the HTTP request on cleanup.
|
|
@@ -212,6 +234,25 @@ export function CopilotChat({
|
|
|
212
234
|
// connectAgent already emits via the subscriber system, but catch
|
|
213
235
|
// here to prevent unhandled rejections from unexpected errors.
|
|
214
236
|
console.error("CopilotChat: connectAgent failed", error);
|
|
237
|
+
} finally {
|
|
238
|
+
// Whether the connect succeeded or failed, we're no longer in the
|
|
239
|
+
// transitional "connecting" state for this thread — unblock the
|
|
240
|
+
// welcome-screen-suppression so the view can settle.
|
|
241
|
+
//
|
|
242
|
+
// Defer one animation frame so any trailing React commits from the
|
|
243
|
+
// bootstrap replay (final assistant message content) paint before
|
|
244
|
+
// isConnecting flips off. Without this, suggestions + copy button
|
|
245
|
+
// can briefly appear against an incompletely-laid-out message tree
|
|
246
|
+
// and visibly snap once the last text chunk lands.
|
|
247
|
+
if (!detached) {
|
|
248
|
+
const raf =
|
|
249
|
+
typeof requestAnimationFrame === "function"
|
|
250
|
+
? requestAnimationFrame
|
|
251
|
+
: (cb: () => void) => setTimeout(cb, 16);
|
|
252
|
+
raf(() => {
|
|
253
|
+
if (!detached) setLastConnectedThreadId(resolvedThreadId);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
215
256
|
}
|
|
216
257
|
};
|
|
217
258
|
connect(agent);
|
|
@@ -229,7 +270,7 @@ export function CopilotChat({
|
|
|
229
270
|
};
|
|
230
271
|
// copilotkit is intentionally excluded — it is a stable ref that never changes.
|
|
231
272
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
232
|
-
}, [resolvedThreadId, agent, resolvedAgentId]);
|
|
273
|
+
}, [resolvedThreadId, agent, resolvedAgentId, providedThreadId]);
|
|
233
274
|
|
|
234
275
|
const onSubmitInput = useCallback(
|
|
235
276
|
async (value: string) => {
|
|
@@ -497,6 +538,37 @@ export function CopilotChat({
|
|
|
497
538
|
[messagesMemoKey],
|
|
498
539
|
);
|
|
499
540
|
|
|
541
|
+
// Compute the ID of the last user message for scroll-pinning logic.
|
|
542
|
+
const lastUserMessageId = useMemo(() => {
|
|
543
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
544
|
+
if (messages[i].role === "user") return messages[i].id;
|
|
545
|
+
}
|
|
546
|
+
return null;
|
|
547
|
+
}, [messages]);
|
|
548
|
+
|
|
549
|
+
// Track a nonce that increments each time a new user message ID appears.
|
|
550
|
+
// Using useState ensures the context value propagates correctly on the
|
|
551
|
+
// render that follows the state update (approach b from the design doc).
|
|
552
|
+
const [sendNonce, setSendNonce] = useState(0);
|
|
553
|
+
// Seed with the current value so restoring a thread with existing messages
|
|
554
|
+
// does not count as a new send. Only later-render id transitions bump.
|
|
555
|
+
const prevLastUserMessageIdRef = useRef<string | null>(lastUserMessageId);
|
|
556
|
+
|
|
557
|
+
useEffect(() => {
|
|
558
|
+
if (
|
|
559
|
+
lastUserMessageId &&
|
|
560
|
+
lastUserMessageId !== prevLastUserMessageIdRef.current
|
|
561
|
+
) {
|
|
562
|
+
setSendNonce((n) => n + 1);
|
|
563
|
+
prevLastUserMessageIdRef.current = lastUserMessageId;
|
|
564
|
+
}
|
|
565
|
+
}, [lastUserMessageId]);
|
|
566
|
+
|
|
567
|
+
const lastUserMessageState = useMemo<LastUserMessageState>(
|
|
568
|
+
() => ({ id: lastUserMessageId, sendNonce }),
|
|
569
|
+
[lastUserMessageId, sendNonce],
|
|
570
|
+
);
|
|
571
|
+
|
|
500
572
|
const finalProps: CopilotChatViewProps = {
|
|
501
573
|
...mergedProps,
|
|
502
574
|
messages,
|
|
@@ -521,6 +593,8 @@ export function CopilotChat({
|
|
|
521
593
|
onDragOver: handleDragOver,
|
|
522
594
|
onDragLeave: handleDragLeave,
|
|
523
595
|
onDrop: handleDrop,
|
|
596
|
+
isConnecting,
|
|
597
|
+
hasExplicitThreadId: !!providedThreadId,
|
|
524
598
|
};
|
|
525
599
|
|
|
526
600
|
// Always create a provider with merged values
|
|
@@ -564,7 +638,9 @@ export function CopilotChat({
|
|
|
564
638
|
{transcriptionError}
|
|
565
639
|
</div>
|
|
566
640
|
)}
|
|
567
|
-
{
|
|
641
|
+
<LastUserMessageContext.Provider value={lastUserMessageState}>
|
|
642
|
+
{RenderedChatView}
|
|
643
|
+
</LastUserMessageContext.Provider>
|
|
568
644
|
</div>
|
|
569
645
|
</CopilotChatConfigurationProvider>
|
|
570
646
|
);
|
|
@@ -98,7 +98,7 @@ export function CopilotChatAssistantMessage({
|
|
|
98
98
|
thumbsUpButton,
|
|
99
99
|
CopilotChatAssistantMessage.ThumbsUpButton,
|
|
100
100
|
{
|
|
101
|
-
onClick: onThumbsUp,
|
|
101
|
+
onClick: onThumbsUp ? () => onThumbsUp(message) : undefined,
|
|
102
102
|
},
|
|
103
103
|
);
|
|
104
104
|
|
|
@@ -106,7 +106,7 @@ export function CopilotChatAssistantMessage({
|
|
|
106
106
|
thumbsDownButton,
|
|
107
107
|
CopilotChatAssistantMessage.ThumbsDownButton,
|
|
108
108
|
{
|
|
109
|
-
onClick: onThumbsDown,
|
|
109
|
+
onClick: onThumbsDown ? () => onThumbsDown(message) : undefined,
|
|
110
110
|
},
|
|
111
111
|
);
|
|
112
112
|
|
|
@@ -114,7 +114,7 @@ export function CopilotChatAssistantMessage({
|
|
|
114
114
|
readAloudButton,
|
|
115
115
|
CopilotChatAssistantMessage.ReadAloudButton,
|
|
116
116
|
{
|
|
117
|
-
onClick: onReadAloud,
|
|
117
|
+
onClick: onReadAloud ? () => onReadAloud(message) : undefined,
|
|
118
118
|
},
|
|
119
119
|
);
|
|
120
120
|
|
|
@@ -122,7 +122,7 @@ export function CopilotChatAssistantMessage({
|
|
|
122
122
|
regenerateButton,
|
|
123
123
|
CopilotChatAssistantMessage.RegenerateButton,
|
|
124
124
|
{
|
|
125
|
-
onClick: onRegenerate,
|
|
125
|
+
onClick: onRegenerate ? () => onRegenerate(message) : undefined,
|
|
126
126
|
},
|
|
127
127
|
);
|
|
128
128
|
|
|
@@ -87,6 +87,19 @@ type CopilotChatInputRestProps = {
|
|
|
87
87
|
containerRef?: React.Ref<HTMLDivElement>;
|
|
88
88
|
/** Whether to show the disclaimer. Default: true for absolute positioning, false for static */
|
|
89
89
|
showDisclaimer?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Set to `true` when the input sits at the bottom of its container as a
|
|
92
|
+
* flex-last-child (visible position is driven by layout, not CSS
|
|
93
|
+
* positioning). Triggers reservation of bottom space for the fixed
|
|
94
|
+
* CopilotKit license banner via the
|
|
95
|
+
* `--copilotkit-license-banner-offset` CSS var so the two don't overlap.
|
|
96
|
+
*
|
|
97
|
+
* Not needed when `positioning === "absolute"`; that mode already pins the
|
|
98
|
+
* input to the bottom and picks up the same reservation automatically.
|
|
99
|
+
* Leave unset (default `false`) for inputs rendered mid-layout such as the
|
|
100
|
+
* welcome screen, where the banner offset would push the input off-center.
|
|
101
|
+
*/
|
|
102
|
+
bottomAnchored?: boolean;
|
|
90
103
|
} & Omit<React.HTMLAttributes<HTMLDivElement>, "onChange">;
|
|
91
104
|
|
|
92
105
|
type CopilotChatInputBaseProps = WithSlots<
|
|
@@ -130,6 +143,7 @@ export function CopilotChatInput({
|
|
|
130
143
|
keyboardHeight = 0,
|
|
131
144
|
containerRef,
|
|
132
145
|
showDisclaimer,
|
|
146
|
+
bottomAnchored = false,
|
|
133
147
|
textArea,
|
|
134
148
|
sendButton,
|
|
135
149
|
startTranscribeButton,
|
|
@@ -384,6 +398,12 @@ export function CopilotChatInput({
|
|
|
384
398
|
);
|
|
385
399
|
|
|
386
400
|
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
401
|
+
// Skip key handling during IME composition (e.g. CJK input).
|
|
402
|
+
// The compositionend event will fire separately when composition ends.
|
|
403
|
+
if (e.nativeEvent.isComposing || e.keyCode === 229) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
387
407
|
if (commandQuery !== null && mode === "input") {
|
|
388
408
|
if (e.key === "ArrowDown") {
|
|
389
409
|
if (filteredCommands.length > 0) {
|
|
@@ -455,10 +475,12 @@ export function CopilotChatInput({
|
|
|
455
475
|
|
|
456
476
|
onSubmitMessage(trimmed);
|
|
457
477
|
|
|
478
|
+
// Always clear the input after sending, including controlled mode.
|
|
479
|
+
// In controlled mode, onChange("") notifies the parent to reset its state.
|
|
458
480
|
if (!isControlled) {
|
|
459
481
|
setInternalValue("");
|
|
460
|
-
onChange?.("");
|
|
461
482
|
}
|
|
483
|
+
onChange?.("");
|
|
462
484
|
|
|
463
485
|
if (inputRef.current) {
|
|
464
486
|
inputRef.current.focus();
|
|
@@ -470,6 +492,12 @@ export function CopilotChatInput({
|
|
|
470
492
|
value: resolvedValue,
|
|
471
493
|
onChange: handleChange,
|
|
472
494
|
onKeyDown: handleKeyDown,
|
|
495
|
+
onCompositionStart: () => {
|
|
496
|
+
isComposingRef.current = true;
|
|
497
|
+
},
|
|
498
|
+
onCompositionEnd: () => {
|
|
499
|
+
isComposingRef.current = false;
|
|
500
|
+
},
|
|
473
501
|
autoFocus: autoFocus,
|
|
474
502
|
className: twMerge(
|
|
475
503
|
"cpk:w-full cpk:py-3",
|
|
@@ -612,9 +640,14 @@ export function CopilotChatInput({
|
|
|
612
640
|
}
|
|
613
641
|
};
|
|
614
642
|
|
|
643
|
+
// Track whether an IME composition is active so we can avoid
|
|
644
|
+
// resetting textarea.value during measurement (which would break
|
|
645
|
+
// the composition session).
|
|
646
|
+
const isComposingRef = useRef(false);
|
|
647
|
+
|
|
615
648
|
const ensureMeasurements = useCallback(() => {
|
|
616
649
|
const textarea = inputRef.current;
|
|
617
|
-
if (!textarea) {
|
|
650
|
+
if (!textarea || isComposingRef.current) {
|
|
618
651
|
return;
|
|
619
652
|
}
|
|
620
653
|
|
|
@@ -1078,6 +1111,14 @@ export function CopilotChatInput({
|
|
|
1078
1111
|
transform:
|
|
1079
1112
|
keyboardHeight > 0 ? `translateY(-${keyboardHeight}px)` : undefined,
|
|
1080
1113
|
transition: "transform 0.2s ease-out",
|
|
1114
|
+
// Reserve room when the fixed license banner is visible so it doesn't
|
|
1115
|
+
// overlap the input. Applied only for bottom-anchored inputs (either
|
|
1116
|
+
// `positioning === "absolute"`, or an explicitly-flagged flex-last-child
|
|
1117
|
+
// input in run state). The welcome-screen input sits mid-layout and
|
|
1118
|
+
// must stay still when the banner is present.
|
|
1119
|
+
...(positioning === "absolute" || bottomAnchored
|
|
1120
|
+
? { paddingBottom: "var(--copilotkit-license-banner-offset, 0px)" }
|
|
1121
|
+
: {}),
|
|
1081
1122
|
}}
|
|
1082
1123
|
{...props}
|
|
1083
1124
|
>
|