@copilotkit/react-core 1.56.1 → 1.56.2-canary.test-welcome-screen

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.
Files changed (57) hide show
  1. package/dist/{copilotkit-CSJw5BG8.cjs → copilotkit-By2G6-Zx.cjs} +250 -63
  2. package/dist/copilotkit-By2G6-Zx.cjs.map +1 -0
  3. package/dist/{copilotkit-CCbxm6JM.d.mts → copilotkit-DFaI4j2r.d.mts} +64 -18
  4. package/dist/copilotkit-DFaI4j2r.d.mts.map +1 -0
  5. package/dist/{copilotkit-BtP7w7cT.d.cts → copilotkit-Dg4r4Gi_.d.cts} +64 -18
  6. package/dist/copilotkit-Dg4r4Gi_.d.cts.map +1 -0
  7. package/dist/{copilotkit-Cj2ZIxVr.mjs → copilotkit-PzJlPKcU.mjs} +251 -64
  8. package/dist/copilotkit-PzJlPKcU.mjs.map +1 -0
  9. package/dist/index.cjs +1 -1
  10. package/dist/index.d.cts +2 -1
  11. package/dist/index.d.cts.map +1 -1
  12. package/dist/index.d.mts +2 -1
  13. package/dist/index.d.mts.map +1 -1
  14. package/dist/index.mjs +1 -1
  15. package/dist/index.umd.js +31 -44
  16. package/dist/index.umd.js.map +1 -1
  17. package/dist/v2/index.cjs +1 -1
  18. package/dist/v2/index.css +1 -1
  19. package/dist/v2/index.d.cts +2 -2
  20. package/dist/v2/index.d.mts +2 -2
  21. package/dist/v2/index.mjs +1 -1
  22. package/dist/v2/index.umd.js +249 -66
  23. package/dist/v2/index.umd.js.map +1 -1
  24. package/package.json +6 -6
  25. package/src/components/copilot-provider/__tests__/v1-explicit-threadid-bridge.test.tsx +107 -0
  26. package/src/components/copilot-provider/copilotkit.tsx +6 -1
  27. package/src/context/__tests__/threads-context.test.tsx +116 -3
  28. package/src/context/threads-context.tsx +18 -1
  29. package/src/v2/components/chat/CopilotChat.tsx +91 -4
  30. package/src/v2/components/chat/CopilotChatInput.tsx +22 -0
  31. package/src/v2/components/chat/CopilotChatView.tsx +206 -11
  32. package/src/v2/components/chat/__tests__/CopilotChat.absentThreadConnect.test.tsx +66 -0
  33. package/src/v2/components/chat/__tests__/CopilotChat.welcomeGate.test.tsx +186 -0
  34. package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +300 -2
  35. package/src/v2/components/chat/__tests__/CopilotChatView.connectingGate.test.tsx +56 -0
  36. package/src/v2/components/chat/__tests__/CopilotChatView.pinToSend.test.tsx +94 -0
  37. package/src/v2/components/chat/__tests__/copilot-chat-throttle.test.tsx +0 -1
  38. package/src/v2/components/chat/__tests__/normalize-auto-scroll.test.ts +37 -0
  39. package/src/v2/components/chat/index.ts +2 -0
  40. package/src/v2/components/chat/last-user-message-context.ts +21 -0
  41. package/src/v2/components/chat/normalize-auto-scroll.ts +17 -0
  42. package/src/v2/components/license-warning-banner.tsx +20 -1
  43. package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +6 -0
  44. package/src/v2/hooks/__tests__/use-agent-thread-isolation.test.tsx +6 -0
  45. package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +76 -50
  46. package/src/v2/hooks/__tests__/use-pin-to-send.test.tsx +219 -0
  47. package/src/v2/hooks/__tests__/use-threads.test.tsx +68 -0
  48. package/src/v2/hooks/use-agent.tsx +34 -77
  49. package/src/v2/hooks/use-pin-to-send.ts +94 -0
  50. package/src/v2/hooks/use-threads.tsx +55 -12
  51. package/src/v2/providers/CopilotChatConfigurationProvider.tsx +29 -1
  52. package/src/v2/providers/CopilotKitProvider.tsx +2 -11
  53. package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.tsx +106 -0
  54. package/dist/copilotkit-BtP7w7cT.d.cts.map +0 -1
  55. package/dist/copilotkit-CCbxm6JM.d.mts.map +0 -1
  56. package/dist/copilotkit-CSJw5BG8.cjs.map +0 -1
  57. package/dist/copilotkit-Cj2ZIxVr.mjs.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copilotkit/react-core",
3
- "version": "1.56.1",
3
+ "version": "1.56.2-canary.test-welcome-screen",
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/runtime-client-gql": "1.56.1",
77
- "@copilotkit/core": "1.56.1",
78
- "@copilotkit/shared": "1.56.1",
79
- "@copilotkit/web-inspector": "1.56.1",
80
- "@copilotkit/a2ui-renderer": "1.56.1"
76
+ "@copilotkit/a2ui-renderer": "1.56.2-canary.test-welcome-screen",
77
+ "@copilotkit/core": "1.56.2-canary.test-welcome-screen",
78
+ "@copilotkit/shared": "1.56.2-canary.test-welcome-screen",
79
+ "@copilotkit/web-inspector": "1.56.2-canary.test-welcome-screen",
80
+ "@copilotkit/runtime-client-gql": "1.56.2-canary.test-welcome-screen"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@tailwindcss/cli": "^4.1.11",
@@ -0,0 +1,107 @@
1
+ import React from "react";
2
+ import { render, screen, act } from "@testing-library/react";
3
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
4
+ import { CopilotKit } from "../copilotkit";
5
+ import { useCopilotContext } from "../../../context/copilot-context";
6
+ import { useCopilotChatConfiguration } from "../../../v2";
7
+ import type { CopilotKitProps } from "../copilotkit-props";
8
+
9
+ /**
10
+ * Probe that reads hasExplicitThreadId from the CopilotChatConfigurationProvider
11
+ * that the v1 <CopilotKit> bridge renders. This is the surface CopilotChat
12
+ * itself reads from to gate /connect and the welcome screen.
13
+ */
14
+ function ExplicitProbe() {
15
+ const config = useCopilotChatConfiguration();
16
+ return (
17
+ <>
18
+ <div data-testid="explicit">{String(config?.hasExplicitThreadId)}</div>
19
+ <div data-testid="threadId">{config?.threadId ?? ""}</div>
20
+ </>
21
+ );
22
+ }
23
+
24
+ /**
25
+ * Exposes the v1 context's setThreadId so tests can drive the
26
+ * auto → explicit transition from outside React.
27
+ */
28
+ function SetThreadIdButton({ nextId }: { nextId: string }) {
29
+ const { setThreadId } = useCopilotContext();
30
+ return (
31
+ <button data-testid="setThread" onClick={() => setThreadId(nextId)}>
32
+ set
33
+ </button>
34
+ );
35
+ }
36
+
37
+ // `agents__unsafe_dev_only` isn't declared on v1 CopilotKitProps but is
38
+ // forwarded via spread to the v2 provider underneath. Cast once here rather
39
+ // than every render.
40
+ type V1Props = CopilotKitProps & {
41
+ agents__unsafe_dev_only?: Record<string, unknown>;
42
+ };
43
+ const CopilotKitAny = CopilotKit as unknown as React.FC<V1Props>;
44
+
45
+ /**
46
+ * Regression coverage for fix/welcome-not-showing-at-all at the v1 bridge
47
+ * boundary. The v1 <CopilotKit> wrapper pipes a ThreadsProvider-minted UUID
48
+ * through as `threadId`, but that UUID is NOT a caller choice — the bridge
49
+ * must mark it as non-explicit so downstream consumers don't treat it as a
50
+ * real backend thread. These tests verify the signal makes it all the way
51
+ * through to CopilotChatConfigurationProvider.
52
+ */
53
+ describe("v1 <CopilotKit> bridge → hasExplicitThreadId", () => {
54
+ // Silence the in-dev/test "missing runtimeUrl" warning — we pass publicApiKey.
55
+ let warnSpy: ReturnType<typeof vi.spyOn>;
56
+ beforeEach(() => {
57
+ warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
58
+ });
59
+ afterEach(() => {
60
+ warnSpy.mockRestore();
61
+ });
62
+
63
+ it("is false on mount when no threadId prop is supplied", () => {
64
+ render(
65
+ <CopilotKitAny publicApiKey="test-key">
66
+ <ExplicitProbe />
67
+ </CopilotKitAny>,
68
+ );
69
+
70
+ // ThreadsProvider auto-minted the UUID — it's not a caller-picked thread.
71
+ expect(screen.getByTestId("explicit").textContent).toBe("false");
72
+ // threadId still resolves to a value (mock-thread-id from setupTests),
73
+ // but downstream consumers must NOT treat it as real.
74
+ expect(screen.getByTestId("threadId").textContent).toBe("mock-thread-id");
75
+ });
76
+
77
+ it("is true when threadId prop is supplied to <CopilotKit>", () => {
78
+ render(
79
+ <CopilotKitAny publicApiKey="test-key" threadId="caller-thread">
80
+ <ExplicitProbe />
81
+ </CopilotKitAny>,
82
+ );
83
+
84
+ expect(screen.getByTestId("explicit").textContent).toBe("true");
85
+ expect(screen.getByTestId("threadId").textContent).toBe("caller-thread");
86
+ });
87
+
88
+ it("flips from false to true after setThreadId() is called on the v1 context", () => {
89
+ render(
90
+ <CopilotKitAny publicApiKey="test-key">
91
+ <ExplicitProbe />
92
+ <SetThreadIdButton nextId="user-picked-thread" />
93
+ </CopilotKitAny>,
94
+ );
95
+
96
+ expect(screen.getByTestId("explicit").textContent).toBe("false");
97
+
98
+ act(() => {
99
+ screen.getByTestId("setThread").click();
100
+ });
101
+
102
+ expect(screen.getByTestId("threadId").textContent).toBe(
103
+ "user-picked-thread",
104
+ );
105
+ expect(screen.getByTestId("explicit").textContent).toBe("true");
106
+ });
107
+ });
@@ -478,7 +478,11 @@ export function CopilotKitInternal(cpkProps: CopilotKitProps) {
478
478
  }
479
479
  }, [props.agent]);
480
480
 
481
- const { threadId, setThreadId: setInternalThreadId } = useThreads();
481
+ const {
482
+ threadId,
483
+ setThreadId: setInternalThreadId,
484
+ isThreadIdExplicit,
485
+ } = useThreads();
482
486
 
483
487
  const setThreadId = useCallback(
484
488
  (value: SetStateAction<string>) => {
@@ -757,6 +761,7 @@ export function CopilotKitInternal(cpkProps: CopilotKitProps) {
757
761
  // isModalDefaultOpen={isModalDefaultOpen}
758
762
  agentId={props.agent ?? "default"}
759
763
  threadId={threadId}
764
+ hasExplicitThreadId={isThreadIdExplicit}
760
765
  >
761
766
  <CopilotContext.Provider value={copilotContextValue}>
762
767
  <CopilotListeners />
@@ -1,10 +1,26 @@
1
1
  import React from "react";
2
- import { render, screen } from "@testing-library/react";
2
+ import { render, screen, act } from "@testing-library/react";
3
+ import { describe, it, expect } from "vitest";
3
4
  import { ThreadsProvider, useThreads } from "../threads-context";
4
5
 
5
6
  function ThreadIdViewer() {
6
- const { threadId } = useThreads();
7
- return <div data-testid="threadId">{threadId}</div>;
7
+ const { threadId, isThreadIdExplicit } = useThreads();
8
+ return (
9
+ <>
10
+ <div data-testid="threadId">{threadId}</div>
11
+ <div data-testid="isExplicit">{String(isThreadIdExplicit)}</div>
12
+ </>
13
+ );
14
+ }
15
+
16
+ // Exposes setThreadId to the test so it can trigger the auto→explicit flip.
17
+ function ThreadIdController({ nextId }: { nextId: string }) {
18
+ const { setThreadId } = useThreads();
19
+ return (
20
+ <button data-testid="setThread" onClick={() => setThreadId(nextId)}>
21
+ set
22
+ </button>
23
+ );
8
24
  }
9
25
 
10
26
  describe("ThreadsProvider", () => {
@@ -25,4 +41,101 @@ describe("ThreadsProvider", () => {
25
41
 
26
42
  expect(screen.getByTestId("threadId").textContent).toBe("customer-thread");
27
43
  });
44
+
45
+ describe("isThreadIdExplicit", () => {
46
+ it("is false on first mount when no threadId prop is supplied", () => {
47
+ // Auto-minted UUID — the backend has never seen it, so downstream
48
+ // consumers (e.g. /connect) must NOT treat this as a real thread.
49
+ render(
50
+ <ThreadsProvider>
51
+ <ThreadIdViewer />
52
+ </ThreadsProvider>,
53
+ );
54
+
55
+ expect(screen.getByTestId("threadId").textContent).toBe("mock-thread-id");
56
+ expect(screen.getByTestId("isExplicit").textContent).toBe("false");
57
+ });
58
+
59
+ it("is true when threadId prop is supplied on mount", () => {
60
+ render(
61
+ <ThreadsProvider threadId="customer-thread">
62
+ <ThreadIdViewer />
63
+ </ThreadsProvider>,
64
+ );
65
+
66
+ expect(screen.getByTestId("threadId").textContent).toBe(
67
+ "customer-thread",
68
+ );
69
+ expect(screen.getByTestId("isExplicit").textContent).toBe("true");
70
+ });
71
+
72
+ it("flips from false to true after setThreadId() is called", () => {
73
+ render(
74
+ <ThreadsProvider>
75
+ <ThreadIdViewer />
76
+ <ThreadIdController nextId="user-picked-thread" />
77
+ </ThreadsProvider>,
78
+ );
79
+
80
+ expect(screen.getByTestId("isExplicit").textContent).toBe("false");
81
+
82
+ act(() => {
83
+ screen.getByTestId("setThread").click();
84
+ });
85
+
86
+ expect(screen.getByTestId("threadId").textContent).toBe(
87
+ "user-picked-thread",
88
+ );
89
+ expect(screen.getByTestId("isExplicit").textContent).toBe("true");
90
+ });
91
+
92
+ it("reverts to false when an explicit prop is removed and setThreadId was never called", () => {
93
+ // Current contract: explicitness via the `threadId` prop is prop-derived,
94
+ // so removing the prop returns the provider to its auto-minted state.
95
+ // Pinning this guards against an accidental "sticky explicit" regression.
96
+ const { rerender } = render(
97
+ <ThreadsProvider threadId="customer-thread">
98
+ <ThreadIdViewer />
99
+ </ThreadsProvider>,
100
+ );
101
+
102
+ expect(screen.getByTestId("isExplicit").textContent).toBe("true");
103
+
104
+ rerender(
105
+ <ThreadsProvider>
106
+ <ThreadIdViewer />
107
+ </ThreadsProvider>,
108
+ );
109
+
110
+ expect(screen.getByTestId("threadId").textContent).toBe("mock-thread-id");
111
+ expect(screen.getByTestId("isExplicit").textContent).toBe("false");
112
+ });
113
+
114
+ it("stays true after prop is removed if setThreadId was called while prop was present", () => {
115
+ // Once the caller has touched setThreadId, explicitness is sticky —
116
+ // the internal "user picked a thread" flag outlives any prop churn.
117
+ const { rerender } = render(
118
+ <ThreadsProvider threadId="customer-thread">
119
+ <ThreadIdViewer />
120
+ <ThreadIdController nextId="user-picked-thread" />
121
+ </ThreadsProvider>,
122
+ );
123
+
124
+ act(() => {
125
+ screen.getByTestId("setThread").click();
126
+ });
127
+
128
+ rerender(
129
+ <ThreadsProvider>
130
+ <ThreadIdViewer />
131
+ <ThreadIdController nextId="user-picked-thread" />
132
+ </ThreadsProvider>,
133
+ );
134
+
135
+ expect(screen.getByTestId("threadId").textContent).toBe(
136
+ "user-picked-thread",
137
+ );
138
+ expect(screen.getByTestId("isExplicit").textContent).toBe("true");
139
+ });
140
+ });
28
141
  });
@@ -1,5 +1,6 @@
1
1
  import React, {
2
2
  createContext,
3
+ useCallback,
3
4
  useContext,
4
5
  useState,
5
6
  ReactNode,
@@ -10,6 +11,12 @@ import { randomUUID } from "@copilotkit/shared";
10
11
  export interface ThreadsContextValue {
11
12
  threadId: string;
12
13
  setThreadId: (value: SetStateAction<string>) => void;
14
+ // True when the current threadId was chosen by the caller — either via
15
+ // the `threadId` prop on <CopilotKit> / <ThreadsProvider>, or via a later
16
+ // setThreadId() call. False when the provider minted a UUID on first
17
+ // mount so downstream consumers don't have to treat that placeholder as
18
+ // a real backend thread.
19
+ isThreadIdExplicit: boolean;
13
20
  }
14
21
 
15
22
  const ThreadsContext = createContext<ThreadsContextValue | undefined>(
@@ -25,15 +32,25 @@ export function ThreadsProvider({
25
32
  children,
26
33
  threadId: explicitThreadId,
27
34
  }: ThreadsProviderProps) {
28
- const [internalThreadId, setThreadId] = useState<string>(() => randomUUID());
35
+ const [internalThreadId, setInternalThreadId] = useState<string>(() =>
36
+ randomUUID(),
37
+ );
38
+ const [internalIsExplicit, setInternalIsExplicit] = useState<boolean>(false);
29
39
 
30
40
  const threadId = explicitThreadId ?? internalThreadId;
41
+ const isThreadIdExplicit = explicitThreadId != null || internalIsExplicit;
42
+
43
+ const setThreadId = useCallback((value: SetStateAction<string>) => {
44
+ setInternalThreadId(value);
45
+ setInternalIsExplicit(true);
46
+ }, []);
31
47
 
32
48
  return (
33
49
  <ThreadsContext.Provider
34
50
  value={{
35
51
  threadId,
36
52
  setThreadId,
53
+ isThreadIdExplicit,
37
54
  }}
38
55
  >
39
56
  {children}
@@ -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,10 +101,20 @@ 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
- () => threadId ?? existingConfig?.threadId ?? randomUUID(),
102
- [threadId, existingConfig?.threadId],
106
+ () => providedThreadId ?? randomUUID(),
107
+ [providedThreadId],
103
108
  );
109
+ // "Explicit" means a caller actually picked this thread — via the
110
+ // `threadId` prop on CopilotChat or a wrapping provider that marked its
111
+ // threadId as caller-chosen. An auto-minted UUID leaking down through a
112
+ // CopilotChatConfigurationProvider (e.g. from the v1 CopilotKit →
113
+ // ThreadsProvider chain) does NOT count; treating it as explicit is
114
+ // what made /connect fire against 404s and the welcome screen stay
115
+ // hidden for fresh empty chats.
116
+ const hasExplicitThreadId =
117
+ !!threadId || !!existingConfig?.hasExplicitThreadId;
104
118
 
105
119
  const { agent } = useAgent({
106
120
  agentId: resolvedAgentId,
@@ -191,7 +205,25 @@ export function CopilotChat({
191
205
  ...restProps
192
206
  } = props;
193
207
 
208
+ // Tracks the last threadId for which connectAgent has completed (success or
209
+ // failure). When the user supplies a threadId, we're in "resume existing
210
+ // thread" mode — the welcome screen should be suppressed until the connect
211
+ // resolves, otherwise switching threads flashes the welcome screen while the
212
+ // new thread's messages are still en route.
213
+ const [lastConnectedThreadId, setLastConnectedThreadId] = useState<
214
+ string | null
215
+ >(null);
216
+ const isConnecting =
217
+ hasExplicitThreadId && lastConnectedThreadId !== resolvedThreadId;
218
+
194
219
  useEffect(() => {
220
+ // When the caller hasn't picked a specific thread, resolvedThreadId is a
221
+ // UUID minted locally (either in this CopilotChat or in a wrapping
222
+ // ThreadsProvider). The backend has never seen it, so /connect would
223
+ // always 404 — skip the call. A real thread is only created once the
224
+ // user runs the agent for the first time.
225
+ if (!hasExplicitThreadId) return;
226
+
195
227
  let detached = false;
196
228
 
197
229
  // Create a fresh AbortController so we can cancel the HTTP request on cleanup.
@@ -212,6 +244,25 @@ export function CopilotChat({
212
244
  // connectAgent already emits via the subscriber system, but catch
213
245
  // here to prevent unhandled rejections from unexpected errors.
214
246
  console.error("CopilotChat: connectAgent failed", error);
247
+ } finally {
248
+ // Whether the connect succeeded or failed, we're no longer in the
249
+ // transitional "connecting" state for this thread — unblock the
250
+ // welcome-screen-suppression so the view can settle.
251
+ //
252
+ // Defer one animation frame so any trailing React commits from the
253
+ // bootstrap replay (final assistant message content) paint before
254
+ // isConnecting flips off. Without this, suggestions + copy button
255
+ // can briefly appear against an incompletely-laid-out message tree
256
+ // and visibly snap once the last text chunk lands.
257
+ if (!detached) {
258
+ const raf =
259
+ typeof requestAnimationFrame === "function"
260
+ ? requestAnimationFrame
261
+ : (cb: () => void) => setTimeout(cb, 16);
262
+ raf(() => {
263
+ if (!detached) setLastConnectedThreadId(resolvedThreadId);
264
+ });
265
+ }
215
266
  }
216
267
  };
217
268
  connect(agent);
@@ -229,7 +280,7 @@ export function CopilotChat({
229
280
  };
230
281
  // copilotkit is intentionally excluded — it is a stable ref that never changes.
231
282
  // eslint-disable-next-line react-hooks/exhaustive-deps
232
- }, [resolvedThreadId, agent, resolvedAgentId]);
283
+ }, [resolvedThreadId, agent, resolvedAgentId, hasExplicitThreadId]);
233
284
 
234
285
  const onSubmitInput = useCallback(
235
286
  async (value: string) => {
@@ -497,6 +548,37 @@ export function CopilotChat({
497
548
  [messagesMemoKey],
498
549
  );
499
550
 
551
+ // Compute the ID of the last user message for scroll-pinning logic.
552
+ const lastUserMessageId = useMemo(() => {
553
+ for (let i = messages.length - 1; i >= 0; i--) {
554
+ if (messages[i].role === "user") return messages[i].id;
555
+ }
556
+ return null;
557
+ }, [messages]);
558
+
559
+ // Track a nonce that increments each time a new user message ID appears.
560
+ // Using useState ensures the context value propagates correctly on the
561
+ // render that follows the state update (approach b from the design doc).
562
+ const [sendNonce, setSendNonce] = useState(0);
563
+ // Seed with the current value so restoring a thread with existing messages
564
+ // does not count as a new send. Only later-render id transitions bump.
565
+ const prevLastUserMessageIdRef = useRef<string | null>(lastUserMessageId);
566
+
567
+ useEffect(() => {
568
+ if (
569
+ lastUserMessageId &&
570
+ lastUserMessageId !== prevLastUserMessageIdRef.current
571
+ ) {
572
+ setSendNonce((n) => n + 1);
573
+ prevLastUserMessageIdRef.current = lastUserMessageId;
574
+ }
575
+ }, [lastUserMessageId]);
576
+
577
+ const lastUserMessageState = useMemo<LastUserMessageState>(
578
+ () => ({ id: lastUserMessageId, sendNonce }),
579
+ [lastUserMessageId, sendNonce],
580
+ );
581
+
500
582
  const finalProps: CopilotChatViewProps = {
501
583
  ...mergedProps,
502
584
  messages,
@@ -521,6 +603,8 @@ export function CopilotChat({
521
603
  onDragOver: handleDragOver,
522
604
  onDragLeave: handleDragLeave,
523
605
  onDrop: handleDrop,
606
+ isConnecting,
607
+ hasExplicitThreadId,
524
608
  };
525
609
 
526
610
  // Always create a provider with merged values
@@ -531,6 +615,7 @@ export function CopilotChat({
531
615
  <CopilotChatConfigurationProvider
532
616
  agentId={resolvedAgentId}
533
617
  threadId={resolvedThreadId}
618
+ hasExplicitThreadId={hasExplicitThreadId}
534
619
  labels={labels}
535
620
  isModalDefaultOpen={isModalDefaultOpen}
536
621
  >
@@ -564,7 +649,9 @@ export function CopilotChat({
564
649
  {transcriptionError}
565
650
  </div>
566
651
  )}
567
- {RenderedChatView}
652
+ <LastUserMessageContext.Provider value={lastUserMessageState}>
653
+ {RenderedChatView}
654
+ </LastUserMessageContext.Provider>
568
655
  </div>
569
656
  </CopilotChatConfigurationProvider>
570
657
  );
@@ -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,
@@ -1097,6 +1111,14 @@ export function CopilotChatInput({
1097
1111
  transform:
1098
1112
  keyboardHeight > 0 ? `translateY(-${keyboardHeight}px)` : undefined,
1099
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
+ : {}),
1100
1122
  }}
1101
1123
  {...props}
1102
1124
  >