@copilotkit/react-core 1.56.2 → 1.56.4-canary.1777529757

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 (63) hide show
  1. package/dist/{copilotkit-CSJw5BG8.cjs → copilotkit-BAkj3zUc.cjs} +359 -157
  2. package/dist/copilotkit-BAkj3zUc.cjs.map +1 -0
  3. package/dist/{copilotkit-Cj2ZIxVr.mjs → copilotkit-DAatqMh2.mjs} +360 -158
  4. package/dist/copilotkit-DAatqMh2.mjs.map +1 -0
  5. package/dist/{copilotkit-CCbxm6JM.d.mts → copilotkit-DFaI4j2r.d.mts} +64 -18
  6. package/dist/copilotkit-DFaI4j2r.d.mts.map +1 -0
  7. package/dist/{copilotkit-BtP7w7cT.d.cts → copilotkit-Dg4r4Gi_.d.cts} +64 -18
  8. package/dist/copilotkit-Dg4r4Gi_.d.cts.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 +361 -163
  23. package/dist/v2/index.umd.js.map +1 -1
  24. package/package.json +8 -8
  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/CopilotChatAttachmentQueue.tsx +7 -114
  31. package/src/v2/components/chat/CopilotChatAttachmentRenderer.tsx +26 -6
  32. package/src/v2/components/chat/CopilotChatInput.tsx +22 -0
  33. package/src/v2/components/chat/CopilotChatUserMessage.tsx +2 -2
  34. package/src/v2/components/chat/CopilotChatView.tsx +226 -48
  35. package/src/v2/components/chat/Lightbox.tsx +103 -0
  36. package/src/v2/components/chat/__tests__/CopilotChat.absentThreadConnect.test.tsx +66 -0
  37. package/src/v2/components/chat/__tests__/CopilotChat.suggestionsAlways.test.tsx +189 -0
  38. package/src/v2/components/chat/__tests__/CopilotChat.welcomeGate.test.tsx +186 -0
  39. package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +438 -4
  40. package/src/v2/components/chat/__tests__/CopilotChatView.connectingGate.test.tsx +56 -0
  41. package/src/v2/components/chat/__tests__/CopilotChatView.inputOverlay.test.tsx +264 -0
  42. package/src/v2/components/chat/__tests__/CopilotChatView.pinToSend.test.tsx +94 -0
  43. package/src/v2/components/chat/__tests__/copilot-chat-throttle.test.tsx +0 -1
  44. package/src/v2/components/chat/__tests__/normalize-auto-scroll.test.ts +37 -0
  45. package/src/v2/components/chat/index.ts +2 -0
  46. package/src/v2/components/chat/last-user-message-context.ts +21 -0
  47. package/src/v2/components/chat/normalize-auto-scroll.ts +17 -0
  48. package/src/v2/components/license-warning-banner.tsx +20 -1
  49. package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +6 -0
  50. package/src/v2/hooks/__tests__/use-agent-thread-isolation.test.tsx +6 -0
  51. package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +76 -50
  52. package/src/v2/hooks/__tests__/use-pin-to-send.test.tsx +219 -0
  53. package/src/v2/hooks/__tests__/use-threads.test.tsx +68 -0
  54. package/src/v2/hooks/use-agent.tsx +34 -77
  55. package/src/v2/hooks/use-pin-to-send.ts +94 -0
  56. package/src/v2/hooks/use-threads.tsx +55 -12
  57. package/src/v2/providers/CopilotChatConfigurationProvider.tsx +29 -1
  58. package/src/v2/providers/CopilotKitProvider.tsx +2 -11
  59. package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.tsx +106 -0
  60. package/dist/copilotkit-BtP7w7cT.d.cts.map +0 -1
  61. package/dist/copilotkit-CCbxm6JM.d.mts.map +0 -1
  62. package/dist/copilotkit-CSJw5BG8.cjs.map +0 -1
  63. package/dist/copilotkit-Cj2ZIxVr.mjs.map +0 -1
@@ -45,8 +45,8 @@ let zod = require("zod");
45
45
  let _lit_labs_react = require("@lit-labs/react");
46
46
  let _copilotkit_a2ui_renderer = require("@copilotkit/a2ui-renderer");
47
47
  let zod_to_json_schema = require("zod-to-json-schema");
48
- let _tanstack_react_virtual = require("@tanstack/react-virtual");
49
48
  let react_dom = require("react-dom");
49
+ let _tanstack_react_virtual = require("@tanstack/react-virtual");
50
50
  let use_stick_to_bottom = require("use-stick-to-bottom");
51
51
  let react_markdown = require("react-markdown");
52
52
  react_markdown = __toESM(react_markdown);
@@ -175,7 +175,7 @@ const CopilotChatDefaultLabels = {
175
175
  welcomeMessageText: "How can I help you today?"
176
176
  };
177
177
  const CopilotChatConfiguration = (0, react.createContext)(null);
178
- const CopilotChatConfigurationProvider = ({ children, labels, agentId, threadId, isModalDefaultOpen }) => {
178
+ const CopilotChatConfigurationProvider = ({ children, labels, agentId, threadId, hasExplicitThreadId, isModalDefaultOpen }) => {
179
179
  const parentConfig = (0, react.useContext)(CopilotChatConfiguration);
180
180
  const stableLabels = useShallowStableRef(labels);
181
181
  const mergedLabels = (0, react.useMemo)(() => ({
@@ -189,6 +189,7 @@ const CopilotChatConfigurationProvider = ({ children, labels, agentId, threadId,
189
189
  if (parentConfig?.threadId) return parentConfig.threadId;
190
190
  return (0, _copilotkit_shared.randomUUID)();
191
191
  }, [threadId, parentConfig?.threadId]);
192
+ const resolvedHasExplicitThreadId = (hasExplicitThreadId !== void 0 ? hasExplicitThreadId : !!threadId) || !!parentConfig?.hasExplicitThreadId;
192
193
  const [internalModalOpen, setInternalModalOpen] = (0, react.useState)(isModalDefaultOpen ?? true);
193
194
  const hasExplicitDefault = isModalDefaultOpen !== void 0;
194
195
  const setAndSync = (0, react.useCallback)((open) => {
@@ -211,12 +212,14 @@ const CopilotChatConfigurationProvider = ({ children, labels, agentId, threadId,
211
212
  labels: mergedLabels,
212
213
  agentId: resolvedAgentId,
213
214
  threadId: resolvedThreadId,
215
+ hasExplicitThreadId: resolvedHasExplicitThreadId,
214
216
  isModalOpen: resolvedIsModalOpen,
215
217
  setModalOpen: resolvedSetModalOpen
216
218
  }), [
217
219
  mergedLabels,
218
220
  resolvedAgentId,
219
221
  resolvedThreadId,
222
+ resolvedHasExplicitThreadId,
220
223
  resolvedIsModalOpen,
221
224
  resolvedSetModalOpen
222
225
  ]);
@@ -610,7 +613,7 @@ CopilotChatAudioRecorder.displayName = "CopilotChatAudioRecorder";
610
613
  //#region src/v2/components/chat/CopilotChatInput.tsx
611
614
  const SLASH_MENU_MAX_VISIBLE_ITEMS = 5;
612
615
  const SLASH_MENU_ITEM_HEIGHT_PX = 40;
613
- function CopilotChatInput({ mode = "input", onSubmitMessage, onStop, isRunning = false, onStartTranscribe, onCancelTranscribe, onFinishTranscribe, onFinishTranscribeWithAudio, onAddFile, onChange, value, toolsMenu, autoFocus = false, positioning = "static", keyboardHeight = 0, containerRef, showDisclaimer, textArea, sendButton, startTranscribeButton, cancelTranscribeButton, finishTranscribeButton, addMenuButton, audioRecorder, disclaimer, children, className, ...props }) {
616
+ function CopilotChatInput({ mode = "input", onSubmitMessage, onStop, isRunning = false, onStartTranscribe, onCancelTranscribe, onFinishTranscribe, onFinishTranscribeWithAudio, onAddFile, onChange, value, toolsMenu, autoFocus = false, positioning = "static", keyboardHeight = 0, containerRef, showDisclaimer, bottomAnchored = false, textArea, sendButton, startTranscribeButton, cancelTranscribeButton, finishTranscribeButton, addMenuButton, audioRecorder, disclaimer, children, className, ...props }) {
614
617
  const isControlled = value !== void 0;
615
618
  const [internalValue, setInternalValue] = (0, react.useState)(() => value ?? "");
616
619
  (0, react.useEffect)(() => {
@@ -1134,7 +1137,8 @@ function CopilotChatInput({ mode = "input", onSubmitMessage, onStop, isRunning =
1134
1137
  className: cn("cpk:pointer-events-none cpk:relative cpk:z-20", positioning === "absolute" && "cpk:absolute cpk:bottom-0 cpk:left-0 cpk:right-0", className),
1135
1138
  style: {
1136
1139
  transform: keyboardHeight > 0 ? `translateY(-${keyboardHeight}px)` : void 0,
1137
- transition: "transform 0.2s ease-out"
1140
+ transition: "transform 0.2s ease-out",
1141
+ ...positioning === "absolute" || bottomAnchored ? { paddingBottom: "var(--copilotkit-license-banner-offset, 0px)" } : {}
1138
1142
  },
1139
1143
  ...props,
1140
1144
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -1349,6 +1353,8 @@ CopilotKitInspector.displayName = "CopilotKitInspector";
1349
1353
 
1350
1354
  //#endregion
1351
1355
  //#region src/v2/components/license-warning-banner.tsx
1356
+ const LICENSE_BANNER_OFFSET_PX = 52;
1357
+ const LICENSE_BANNER_OFFSET_VAR = "--copilotkit-license-banner-offset";
1352
1358
  const BANNER_STYLES = {
1353
1359
  base: {
1354
1360
  position: "fixed",
@@ -1390,6 +1396,14 @@ function getSeverityStyle(severity) {
1390
1396
  }
1391
1397
  }
1392
1398
  function BannerShell({ severity, message, actionLabel, actionUrl, onDismiss }) {
1399
+ (0, react.useEffect)(() => {
1400
+ if (typeof document === "undefined") return;
1401
+ const root = document.documentElement;
1402
+ root.style.setProperty(LICENSE_BANNER_OFFSET_VAR, `${LICENSE_BANNER_OFFSET_PX}px`);
1403
+ return () => {
1404
+ root.style.removeProperty(LICENSE_BANNER_OFFSET_VAR);
1405
+ };
1406
+ }, []);
1393
1407
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1394
1408
  style: {
1395
1409
  ...BANNER_STYLES.base,
@@ -3342,7 +3356,6 @@ const CopilotKitProvider = ({ children, runtimeUrl, headers: headersProp = {}, c
3342
3356
  didMountRef.current = true;
3343
3357
  }, []);
3344
3358
  (0, react.useEffect)(() => {
3345
- if (defaultThrottleMs !== void 0 && (!Number.isFinite(defaultThrottleMs) || defaultThrottleMs < 0)) console.error(`CopilotKitProvider: defaultThrottleMs must be a non-negative finite number, got ${defaultThrottleMs}. useAgent hooks without an explicit throttleMs will fall back to unthrottled.`);
3346
3359
  copilotkit.setDefaultThrottleMs(defaultThrottleMs);
3347
3360
  }, [copilotkit, defaultThrottleMs]);
3348
3361
  const designSkill = openGenerativeUI?.designSkill ?? DEFAULT_DESIGN_SKILL;
@@ -3559,15 +3572,6 @@ function useAgent({ agentId, threadId, updates, throttleMs } = {}) {
3559
3572
  const providerThrottleMs = copilotkit.defaultThrottleMs;
3560
3573
  const chatConfig = useCopilotChatConfiguration();
3561
3574
  threadId ??= chatConfig?.threadId;
3562
- const effectiveThrottleMs = (0, react.useMemo)(() => {
3563
- const resolved = throttleMs ?? providerThrottleMs ?? 0;
3564
- if (!Number.isFinite(resolved) || resolved < 0) {
3565
- const source = throttleMs !== void 0 ? "hook-level throttleMs" : "provider-level defaultThrottleMs";
3566
- console.error(`useAgent: ${source} must be a non-negative finite number, got ${resolved}. Falling back to unthrottled.`);
3567
- return 0;
3568
- }
3569
- return resolved;
3570
- }, [throttleMs, providerThrottleMs]);
3571
3575
  const [, forceUpdate] = (0, react.useReducer)((x) => x + 1, 0);
3572
3576
  const updateFlags = (0, react.useMemo)(() => updates ?? ALL_UPDATES, [JSON.stringify(updates)]);
3573
3577
  const provisionalAgentCache = (0, react.useRef)(/* @__PURE__ */ new Map());
@@ -3630,9 +3634,8 @@ function useAgent({ agentId, threadId, updates, throttleMs } = {}) {
3630
3634
  ]);
3631
3635
  (0, react.useEffect)(() => {
3632
3636
  if (updateFlags.length === 0) return;
3633
- const handlers = {};
3634
- let timerId = null;
3635
3637
  let active = true;
3638
+ const handlers = {};
3636
3639
  let batchScheduled = false;
3637
3640
  const batchedForceUpdate = () => {
3638
3641
  if (!active) return;
@@ -3644,46 +3647,24 @@ function useAgent({ agentId, threadId, updates, throttleMs } = {}) {
3644
3647
  });
3645
3648
  }
3646
3649
  };
3647
- if (updateFlags.includes(UseAgentUpdate.OnMessagesChanged)) {
3648
- const ms = effectiveThrottleMs;
3649
- if (ms > 0) {
3650
- let throttleActive = false;
3651
- let pending = false;
3652
- const throttledNotify = () => {
3653
- if (!active) return;
3654
- if (!throttleActive) {
3655
- throttleActive = true;
3656
- pending = false;
3657
- forceUpdate();
3658
- timerId = setTimeout(function trailingEdge() {
3659
- timerId = null;
3660
- if (active && pending) {
3661
- pending = false;
3662
- forceUpdate();
3663
- timerId = setTimeout(trailingEdge, ms);
3664
- } else throttleActive = false;
3665
- }, ms);
3666
- } else pending = true;
3667
- };
3668
- handlers.onMessagesChanged = throttledNotify;
3669
- } else handlers.onMessagesChanged = forceUpdate;
3670
- }
3650
+ if (updateFlags.includes(UseAgentUpdate.OnMessagesChanged)) handlers.onMessagesChanged = forceUpdate;
3671
3651
  if (updateFlags.includes(UseAgentUpdate.OnStateChanged)) handlers.onStateChanged = batchedForceUpdate;
3672
3652
  if (updateFlags.includes(UseAgentUpdate.OnRunStatusChanged)) {
3673
3653
  handlers.onRunInitialized = batchedForceUpdate;
3674
3654
  handlers.onRunFinalized = batchedForceUpdate;
3675
3655
  handlers.onRunFailed = batchedForceUpdate;
3656
+ handlers.onRunErrorEvent = batchedForceUpdate;
3676
3657
  }
3677
- const subscription = agent.subscribe(handlers);
3658
+ const subscription = copilotkit.subscribeToAgentWithOptions(agent, handlers, { throttleMs });
3678
3659
  return () => {
3679
3660
  active = false;
3680
- if (timerId !== null) clearTimeout(timerId);
3681
3661
  subscription.unsubscribe();
3682
3662
  };
3683
3663
  }, [
3684
3664
  agent,
3685
3665
  forceUpdate,
3686
- effectiveThrottleMs,
3666
+ throttleMs,
3667
+ providerThrottleMs,
3687
3668
  updateFlags
3688
3669
  ]);
3689
3670
  (0, react.useEffect)(() => {
@@ -4637,13 +4618,14 @@ function useThreads$1({ agentId, includeArchived, limit }) {
4637
4618
  const { copilotkit } = useCopilotKit();
4638
4619
  const [store] = (0, react.useState)(() => (0, _copilotkit_core.ɵcreateThreadStore)({ fetch: globalThis.fetch }));
4639
4620
  const coreThreads = useThreadStoreSelector(store, _copilotkit_core.ɵselectThreads);
4640
- const threads = (0, react.useMemo)(() => coreThreads.map(({ id, agentId, name, archived, createdAt, updatedAt }) => ({
4621
+ const threads = (0, react.useMemo)(() => coreThreads.map(({ id, agentId, name, archived, createdAt, updatedAt, lastRunAt }) => ({
4641
4622
  id,
4642
4623
  agentId,
4643
4624
  name,
4644
4625
  archived,
4645
4626
  createdAt,
4646
- updatedAt
4627
+ updatedAt,
4628
+ ...lastRunAt !== void 0 ? { lastRunAt } : {}
4647
4629
  })), [coreThreads]);
4648
4630
  const storeIsLoading = useThreadStoreSelector(store, _copilotkit_core.ɵselectThreadsIsLoading);
4649
4631
  const storeError = useThreadStoreSelector(store, _copilotkit_core.ɵselectThreadsError);
@@ -4656,7 +4638,9 @@ function useThreads$1({ agentId, includeArchived, limit }) {
4656
4638
  if (copilotkit.runtimeUrl) return null;
4657
4639
  return /* @__PURE__ */ new Error("Runtime URL is not configured");
4658
4640
  }, [copilotkit.runtimeUrl]);
4659
- const isLoading = runtimeError ? false : storeIsLoading;
4641
+ const [hasDispatchedContext, setHasDispatchedContext] = (0, react.useState)(false);
4642
+ const preConnectLoading = !!copilotkit.runtimeUrl && !hasDispatchedContext;
4643
+ const isLoading = runtimeError ? false : preConnectLoading || storeIsLoading;
4660
4644
  const error = runtimeError ?? storeError;
4661
4645
  (0, react.useEffect)(() => {
4662
4646
  store.start();
@@ -4664,19 +4648,27 @@ function useThreads$1({ agentId, includeArchived, limit }) {
4664
4648
  store.stop();
4665
4649
  };
4666
4650
  }, [store]);
4651
+ const runtimeStatus = copilotkit.runtimeConnectionStatus;
4667
4652
  (0, react.useEffect)(() => {
4668
- const context = copilotkit.runtimeUrl ? {
4653
+ if (!copilotkit.runtimeUrl) {
4654
+ store.setContext(null);
4655
+ return;
4656
+ }
4657
+ if (runtimeStatus !== _copilotkit_core.CopilotKitCoreRuntimeConnectionStatus.Connected) return;
4658
+ const context = {
4669
4659
  runtimeUrl: copilotkit.runtimeUrl,
4670
4660
  headers: { ...copilotkit.headers },
4671
4661
  wsUrl: copilotkit.intelligence?.wsUrl,
4672
4662
  agentId,
4673
4663
  includeArchived,
4674
4664
  limit
4675
- } : null;
4665
+ };
4676
4666
  store.setContext(context);
4667
+ setHasDispatchedContext(true);
4677
4668
  }, [
4678
4669
  store,
4679
4670
  copilotkit.runtimeUrl,
4671
+ runtimeStatus,
4680
4672
  headersKey,
4681
4673
  copilotkit.intelligence?.wsUrl,
4682
4674
  agentId,
@@ -5044,20 +5036,101 @@ CopilotChatAssistantMessage.ReadAloudButton.displayName = "CopilotChatAssistantM
5044
5036
  CopilotChatAssistantMessage.RegenerateButton.displayName = "CopilotChatAssistantMessage.RegenerateButton";
5045
5037
  var CopilotChatAssistantMessage_default = CopilotChatAssistantMessage;
5046
5038
 
5039
+ //#endregion
5040
+ //#region src/v2/components/chat/Lightbox.tsx
5041
+ function Lightbox({ onClose, children }) {
5042
+ (0, react.useEffect)(() => {
5043
+ const handleKey = (e) => {
5044
+ if (e.key === "Escape") onClose();
5045
+ };
5046
+ document.addEventListener("keydown", handleKey);
5047
+ return () => document.removeEventListener("keydown", handleKey);
5048
+ }, [onClose]);
5049
+ if (typeof document === "undefined") return null;
5050
+ return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5051
+ className: "cpk:fixed cpk:inset-0 cpk:z-[9999] cpk:flex cpk:items-center cpk:justify-center cpk:bg-black/80 cpk:animate-fade-in",
5052
+ onClick: onClose,
5053
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5054
+ onClick: onClose,
5055
+ className: "cpk:absolute cpk:top-4 cpk:right-4 cpk:text-white cpk:bg-white/10 cpk:hover:bg-white/20 cpk:rounded-full cpk:w-10 cpk:h-10 cpk:flex cpk:items-center cpk:justify-center cpk:cursor-pointer cpk:border-none cpk:z-10",
5056
+ "aria-label": "Close preview",
5057
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, { className: "cpk:w-5 cpk:h-5" })
5058
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5059
+ onClick: (e) => e.stopPropagation(),
5060
+ children
5061
+ })]
5062
+ }), document.body);
5063
+ }
5064
+ /**
5065
+ * Hook that manages lightbox open/close and uses the View Transition API to
5066
+ * morph the thumbnail into fullscreen content.
5067
+ *
5068
+ * The trick: `view-transition-name` must live on exactly ONE element at a time.
5069
+ * - Old state (thumbnail visible): name is on the thumbnail.
5070
+ * - New state (lightbox visible): name moves to the lightbox content.
5071
+ * `flushSync` ensures React commits the DOM change synchronously inside the
5072
+ * `startViewTransition` callback so the API can snapshot old → new correctly.
5073
+ */
5074
+ function useLightbox() {
5075
+ const thumbnailRef = (0, react.useRef)(null);
5076
+ const [open, setOpen] = (0, react.useState)(false);
5077
+ const vtName = (0, react.useId)();
5078
+ return {
5079
+ thumbnailRef,
5080
+ vtName,
5081
+ open,
5082
+ openLightbox: (0, react.useCallback)(() => {
5083
+ const thumb = thumbnailRef.current;
5084
+ const doc = document;
5085
+ if (doc.startViewTransition && thumb) {
5086
+ thumb.style.viewTransitionName = vtName;
5087
+ doc.startViewTransition(() => {
5088
+ thumb.style.viewTransitionName = "";
5089
+ (0, react_dom.flushSync)(() => setOpen(true));
5090
+ });
5091
+ } else setOpen(true);
5092
+ }, [vtName]),
5093
+ closeLightbox: (0, react.useCallback)(() => {
5094
+ const thumb = thumbnailRef.current;
5095
+ const doc = document;
5096
+ if (doc.startViewTransition && thumb) doc.startViewTransition(() => {
5097
+ (0, react_dom.flushSync)(() => setOpen(false));
5098
+ thumb.style.viewTransitionName = vtName;
5099
+ }).finished.then(() => {
5100
+ thumb.style.viewTransitionName = "";
5101
+ }).catch(() => {
5102
+ thumb.style.viewTransitionName = "";
5103
+ });
5104
+ else setOpen(false);
5105
+ }, [vtName])
5106
+ };
5107
+ }
5108
+
5047
5109
  //#endregion
5048
5110
  //#region src/v2/components/chat/CopilotChatAttachmentRenderer.tsx
5049
5111
  const ImageAttachment = (0, react.memo)(function ImageAttachment({ src, className }) {
5050
5112
  const [error, setError] = (0, react.useState)(false);
5113
+ const { thumbnailRef, vtName, open, openLightbox, closeLightbox } = useLightbox();
5051
5114
  if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5052
5115
  className: cn("cpk:flex cpk:flex-col cpk:items-center cpk:justify-center cpk:rounded-lg cpk:bg-muted cpk:p-4 cpk:text-sm cpk:text-muted-foreground", className),
5053
5116
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "Failed to load image" })
5054
5117
  });
5055
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
5118
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
5119
+ ref: thumbnailRef,
5056
5120
  src,
5057
5121
  alt: "Image attachment",
5058
- className: cn("cpk:max-w-full cpk:h-auto cpk:rounded-lg", className),
5122
+ className: cn("cpk:max-w-[80px] cpk:max-h-[80px] cpk:w-auto cpk:h-auto cpk:rounded-xl cpk:object-cover cpk:cursor-pointer cpk:bg-muted", className),
5123
+ onClick: openLightbox,
5059
5124
  onError: () => setError(true)
5060
- });
5125
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Lightbox, {
5126
+ onClose: closeLightbox,
5127
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
5128
+ style: { viewTransitionName: vtName },
5129
+ src,
5130
+ alt: "Image attachment",
5131
+ className: "cpk:max-w-[90vw] cpk:max-h-[90vh] cpk:object-contain cpk:rounded-lg"
5132
+ })
5133
+ })] });
5061
5134
  });
5062
5135
  const AudioAttachment = (0, react.memo)(function AudioAttachment({ src, filename, className }) {
5063
5136
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -5182,15 +5255,15 @@ function CopilotChatUserMessage({ message, onEditMessage, branchIndex, numberOfB
5182
5255
  "data-message-id": message.id,
5183
5256
  ...props,
5184
5257
  children: [
5185
- BoundMessageRenderer,
5186
5258
  mediaParts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5187
- className: "cpk:flex cpk:flex-col cpk:items-end cpk:gap-2 cpk:mt-2",
5259
+ className: "cpk:flex cpk:flex-row cpk:flex-wrap cpk:justify-end cpk:gap-2 cpk:mb-2",
5188
5260
  children: mediaParts.map((part, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopilotChatAttachmentRenderer, {
5189
5261
  type: part.type,
5190
5262
  source: part.source,
5191
5263
  filename: getFilename(part)
5192
5264
  }, index))
5193
5265
  }),
5266
+ BoundMessageRenderer,
5194
5267
  BoundToolbar
5195
5268
  ]
5196
5269
  });
@@ -5859,6 +5932,7 @@ CopilotChatMessageView.Cursor = function Cursor({ className, ...props }) {
5859
5932
  const CopilotChatAttachmentQueue = ({ attachments, onRemoveAttachment, className }) => {
5860
5933
  if (attachments.length === 0) return null;
5861
5934
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5935
+ "data-testid": "copilot-attachment-queue",
5862
5936
  className: cn("cpk:flex cpk:flex-wrap cpk:gap-2 cpk:p-2", className),
5863
5937
  children: attachments.map((attachment) => {
5864
5938
  const isMedia = attachment.type === "image" || attachment.type === "video";
@@ -5893,73 +5967,6 @@ function AttachmentPreview({ attachment }) {
5893
5967
  case "document": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DocumentPreview, { attachment });
5894
5968
  }
5895
5969
  }
5896
- function Lightbox({ onClose, children }) {
5897
- (0, react.useEffect)(() => {
5898
- const handleKey = (e) => {
5899
- if (e.key === "Escape") onClose();
5900
- };
5901
- document.addEventListener("keydown", handleKey);
5902
- return () => document.removeEventListener("keydown", handleKey);
5903
- }, [onClose]);
5904
- if (typeof document === "undefined") return null;
5905
- return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5906
- className: "cpk:fixed cpk:inset-0 cpk:z-[9999] cpk:flex cpk:items-center cpk:justify-center cpk:bg-black/80 cpk:animate-fade-in",
5907
- onClick: onClose,
5908
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5909
- onClick: onClose,
5910
- className: "cpk:absolute cpk:top-4 cpk:right-4 cpk:text-white cpk:bg-white/10 cpk:hover:bg-white/20 cpk:rounded-full cpk:w-10 cpk:h-10 cpk:flex cpk:items-center cpk:justify-center cpk:cursor-pointer cpk:border-none cpk:z-10",
5911
- "aria-label": "Close preview",
5912
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.X, { className: "cpk:w-5 cpk:h-5" })
5913
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5914
- onClick: (e) => e.stopPropagation(),
5915
- children
5916
- })]
5917
- }), document.body);
5918
- }
5919
- /**
5920
- * Hook that manages lightbox open/close and uses the View Transition API to
5921
- * morph the thumbnail into fullscreen content.
5922
- *
5923
- * The trick: `view-transition-name` must live on exactly ONE element at a time.
5924
- * - Old state (thumbnail visible): name is on the thumbnail.
5925
- * - New state (lightbox visible): name moves to the lightbox content.
5926
- * `flushSync` ensures React commits the DOM change synchronously inside the
5927
- * `startViewTransition` callback so the API can snapshot old → new correctly.
5928
- */
5929
- function useLightbox() {
5930
- const thumbnailRef = (0, react.useRef)(null);
5931
- const [open, setOpen] = (0, react.useState)(false);
5932
- const vtName = (0, react.useId)();
5933
- return {
5934
- thumbnailRef,
5935
- vtName,
5936
- open,
5937
- openLightbox: (0, react.useCallback)(() => {
5938
- const thumb = thumbnailRef.current;
5939
- const doc = document;
5940
- if (doc.startViewTransition && thumb) {
5941
- thumb.style.viewTransitionName = vtName;
5942
- doc.startViewTransition(() => {
5943
- thumb.style.viewTransitionName = "";
5944
- (0, react_dom.flushSync)(() => setOpen(true));
5945
- });
5946
- } else setOpen(true);
5947
- }, []),
5948
- closeLightbox: (0, react.useCallback)(() => {
5949
- const thumb = thumbnailRef.current;
5950
- const doc = document;
5951
- if (doc.startViewTransition && thumb) doc.startViewTransition(() => {
5952
- (0, react_dom.flushSync)(() => setOpen(false));
5953
- thumb.style.viewTransitionName = vtName;
5954
- }).finished.then(() => {
5955
- thumb.style.viewTransitionName = "";
5956
- }).catch(() => {
5957
- thumb.style.viewTransitionName = "";
5958
- });
5959
- else setOpen(false);
5960
- }, [])
5961
- };
5962
- }
5963
5970
  function ImagePreview({ attachment }) {
5964
5971
  const src = (0, _copilotkit_shared.getSourceUrl)(attachment.source);
5965
5972
  const { thumbnailRef, vtName, open, openLightbox, closeLightbox } = useLightbox();
@@ -6201,9 +6208,91 @@ function useKeyboardHeight() {
6201
6208
  return keyboardState;
6202
6209
  }
6203
6210
 
6211
+ //#endregion
6212
+ //#region src/v2/components/chat/normalize-auto-scroll.ts
6213
+ const VALID = [
6214
+ "pin-to-bottom",
6215
+ "pin-to-send",
6216
+ "none"
6217
+ ];
6218
+ function normalizeAutoScroll(value) {
6219
+ if (value === void 0) return "pin-to-bottom";
6220
+ if (value === true) return "pin-to-bottom";
6221
+ if (value === false) return "none";
6222
+ if (VALID.includes(value)) return value;
6223
+ return "pin-to-bottom";
6224
+ }
6225
+
6226
+ //#endregion
6227
+ //#region src/v2/components/chat/last-user-message-context.ts
6228
+ const LastUserMessageContext = react.default.createContext({
6229
+ id: null,
6230
+ sendNonce: 0
6231
+ });
6232
+
6233
+ //#endregion
6234
+ //#region src/v2/hooks/use-pin-to-send.ts
6235
+ function usePinToSend({ scrollRef, contentRef, spacerRef, topOffset = 16 }) {
6236
+ const { id, sendNonce } = (0, react.useContext)(LastUserMessageContext);
6237
+ const lastNonceRef = (0, react.useRef)(-1);
6238
+ const currentSpacerHeightRef = (0, react.useRef)(0);
6239
+ (0, react.useEffect)(() => {
6240
+ if (sendNonce === lastNonceRef.current) return;
6241
+ lastNonceRef.current = sendNonce;
6242
+ if (!id) return;
6243
+ const scrollEl = scrollRef.current;
6244
+ const contentEl = contentRef.current;
6245
+ const spacerEl = spacerRef.current;
6246
+ if (!scrollEl || !contentEl || !spacerEl) return;
6247
+ const escaped = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(id) : id.replace(/[!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, "\\$&");
6248
+ const targetEl = contentEl.querySelector(`[data-message-id="${escaped}"]`);
6249
+ if (!targetEl) return;
6250
+ const viewportHeight = scrollEl.clientHeight;
6251
+ const userMessageHeight = targetEl.getBoundingClientRect().height;
6252
+ const paddingTop = parseFloat(getComputedStyle(targetEl).paddingTop) || 0;
6253
+ const bubbleHeight = Math.max(0, userMessageHeight - paddingTop);
6254
+ const spacerHeight = Math.max(0, viewportHeight - bubbleHeight - topOffset);
6255
+ spacerEl.style.height = `${spacerHeight}px`;
6256
+ currentSpacerHeightRef.current = spacerHeight;
6257
+ const raf = requestAnimationFrame(() => {
6258
+ const targetTop = computeOffsetTop(targetEl, scrollEl) + paddingTop - topOffset;
6259
+ scrollEl.scrollTo({
6260
+ top: Math.max(0, targetTop),
6261
+ behavior: "smooth"
6262
+ });
6263
+ });
6264
+ const ro = new ResizeObserver(() => {
6265
+ if (!contentEl || !spacerEl || !scrollEl) return;
6266
+ const consumedBelow = contentEl.getBoundingClientRect().height - computeOffsetTop(targetEl, contentEl) - userMessageHeight;
6267
+ const remaining = Math.max(0, spacerHeight - consumedBelow);
6268
+ if (remaining < currentSpacerHeightRef.current) {
6269
+ spacerEl.style.height = `${remaining}px`;
6270
+ currentSpacerHeightRef.current = remaining;
6271
+ }
6272
+ });
6273
+ ro.observe(contentEl);
6274
+ return () => {
6275
+ cancelAnimationFrame(raf);
6276
+ ro.disconnect();
6277
+ };
6278
+ }, [
6279
+ id,
6280
+ sendNonce,
6281
+ scrollRef,
6282
+ contentRef,
6283
+ spacerRef,
6284
+ topOffset
6285
+ ]);
6286
+ }
6287
+ function computeOffsetTop(el, stopAt) {
6288
+ const elRect = el.getBoundingClientRect();
6289
+ const stopRect = stopAt.getBoundingClientRect();
6290
+ return elRect.top - stopRect.top + stopAt.scrollTop;
6291
+ }
6292
+
6204
6293
  //#endregion
6205
6294
  //#region src/v2/components/chat/CopilotChatView.tsx
6206
- const FEATHER_HEIGHT = 96;
6295
+ const SCROLL_BUTTON_OFFSET = 16;
6207
6296
  function DropOverlay() {
6208
6297
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6209
6298
  className: cn("cpk:absolute cpk:inset-0 cpk:z-50 cpk:pointer-events-none", "cpk:flex cpk:items-center cpk:justify-center", "cpk:bg-primary/5 cpk:backdrop-blur-[2px]", "cpk:border-2 cpk:border-dashed cpk:border-primary/40 cpk:rounded-lg cpk:m-2"),
@@ -6216,15 +6305,18 @@ function DropOverlay() {
6216
6305
  })
6217
6306
  });
6218
6307
  }
6219
- function CopilotChatView({ messageView, input, scrollView, suggestionView, welcomeScreen, messages = [], autoScroll = true, isRunning = false, suggestions, suggestionLoadingIndexes, onSelectSuggestion, onSubmitMessage, onStop, inputMode, inputValue, onInputChange, onStartTranscribe, onCancelTranscribe, onFinishTranscribe, onFinishTranscribeWithAudio, attachments, onRemoveAttachment, onAddFile, dragOver, onDragOver, onDragLeave, onDrop, disclaimer, children, className, ...props }) {
6220
- const inputContainerRef = (0, react.useRef)(null);
6308
+ function CopilotChatView({ messageView, input, scrollView, suggestionView, welcomeScreen, messages = [], autoScroll = true, isRunning = false, suggestions, suggestionLoadingIndexes, onSelectSuggestion, onSubmitMessage, onStop, inputMode, inputValue, onInputChange, onStartTranscribe, onCancelTranscribe, onFinishTranscribe, onFinishTranscribeWithAudio, attachments, onRemoveAttachment, onAddFile, dragOver, onDragOver, onDragLeave, onDrop, isConnecting = false, hasExplicitThreadId = false, disclaimer, children, className, ...props }) {
6309
+ const [inputContainerEl, setInputContainerEl] = (0, react.useState)(null);
6221
6310
  const [inputContainerHeight, setInputContainerHeight] = (0, react.useState)(0);
6222
6311
  const [isResizing, setIsResizing] = (0, react.useState)(false);
6223
6312
  const resizeTimeoutRef = (0, react.useRef)(null);
6224
6313
  const { isKeyboardOpen, keyboardHeight, availableHeight } = useKeyboardHeight();
6225
6314
  (0, react.useEffect)(() => {
6226
- const element = inputContainerRef.current;
6227
- if (!element) return;
6315
+ const element = inputContainerEl;
6316
+ if (!element) {
6317
+ setInputContainerHeight(0);
6318
+ return;
6319
+ }
6228
6320
  const resizeObserver = new ResizeObserver((entries) => {
6229
6321
  for (const entry of entries) {
6230
6322
  const newHeight = entry.contentRect.height;
@@ -6247,7 +6339,7 @@ function CopilotChatView({ messageView, input, scrollView, suggestionView, welco
6247
6339
  resizeObserver.disconnect();
6248
6340
  if (resizeTimeoutRef.current) clearTimeout(resizeTimeoutRef.current);
6249
6341
  };
6250
- }, []);
6342
+ }, [inputContainerEl]);
6251
6343
  const BoundMessageView = renderSlot(messageView, CopilotChatMessageView, {
6252
6344
  messages,
6253
6345
  isRunning
@@ -6266,11 +6358,11 @@ function CopilotChatView({ messageView, input, scrollView, suggestionView, welco
6266
6358
  onAddFile,
6267
6359
  positioning: "static",
6268
6360
  keyboardHeight: isKeyboardOpen ? keyboardHeight : 0,
6269
- containerRef: inputContainerRef,
6270
6361
  showDisclaimer: true,
6362
+ bottomAnchored: true,
6271
6363
  ...disclaimer !== void 0 ? { disclaimer } : {}
6272
6364
  });
6273
- const hasSuggestions = Array.isArray(suggestions) && suggestions.length > 0;
6365
+ const hasSuggestions = !isConnecting && Array.isArray(suggestions) && suggestions.length > 0;
6274
6366
  const BoundSuggestionView = hasSuggestions ? renderSlot(suggestionView, CopilotChatSuggestionView, {
6275
6367
  suggestions,
6276
6368
  loadingIndexes: suggestionLoadingIndexes,
@@ -6282,7 +6374,8 @@ function CopilotChatView({ messageView, input, scrollView, suggestionView, welco
6282
6374
  inputContainerHeight,
6283
6375
  isResizing,
6284
6376
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6285
- style: { paddingBottom: `${hasSuggestions ? 4 : 32}px` },
6377
+ "data-testid": "copilot-scroll-content",
6378
+ style: { paddingBottom: `${inputContainerHeight + (hasSuggestions ? 4 : 32)}px` },
6286
6379
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6287
6380
  className: "cpk:max-w-3xl cpk:mx-auto",
6288
6381
  children: [BoundMessageView, hasSuggestions ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -6292,7 +6385,7 @@ function CopilotChatView({ messageView, input, scrollView, suggestionView, welco
6292
6385
  })
6293
6386
  })
6294
6387
  });
6295
- if (messages.length === 0 && !(welcomeScreen === false)) {
6388
+ if (messages.length === 0 && !(welcomeScreen === false) && !isConnecting && !hasExplicitThreadId) {
6296
6389
  const BoundInputForWelcome = renderSlot(input, CopilotChatInput_default, {
6297
6390
  onSubmitMessage,
6298
6391
  onStop,
@@ -6356,15 +6449,19 @@ function CopilotChatView({ messageView, input, scrollView, suggestionView, welco
6356
6449
  children: [
6357
6450
  dragOver && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropOverlay, {}),
6358
6451
  BoundScrollView,
6359
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6360
- className: "cpk:max-w-3xl cpk:mx-auto cpk:w-full",
6361
- children: attachments && attachments.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopilotChatAttachmentQueue, {
6362
- attachments,
6363
- onRemoveAttachment: (id) => onRemoveAttachment?.(id),
6364
- className: "cpk:px-4"
6365
- })
6366
- }),
6367
- BoundInput
6452
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6453
+ ref: setInputContainerEl,
6454
+ "data-testid": "copilot-input-overlay",
6455
+ className: "cpk:absolute cpk:bottom-0 cpk:left-0 cpk:right-0 cpk:z-20 cpk:pointer-events-none",
6456
+ children: [attachments && attachments.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6457
+ className: "cpk:max-w-3xl cpk:mx-auto cpk:w-full cpk:pointer-events-auto",
6458
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopilotChatAttachmentQueue, {
6459
+ attachments,
6460
+ onRemoveAttachment: (id) => onRemoveAttachment?.(id),
6461
+ className: "cpk:px-4"
6462
+ })
6463
+ }), BoundInput]
6464
+ })
6368
6465
  ]
6369
6466
  });
6370
6467
  }
@@ -6393,15 +6490,66 @@ function CopilotChatView({ messageView, input, scrollView, suggestionView, welco
6393
6490
  BoundFeather,
6394
6491
  !isAtBottom && !isResizing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6395
6492
  className: "cpk:absolute cpk:inset-x-0 cpk:flex cpk:justify-center cpk:z-30 cpk:pointer-events-none",
6396
- style: { bottom: `${inputContainerHeight + FEATHER_HEIGHT + 16}px` },
6493
+ style: { bottom: `${inputContainerHeight + SCROLL_BUTTON_OFFSET}px` },
6397
6494
  children: renderSlot(scrollToBottomButton, CopilotChatView.ScrollToBottomButton, { onClick: () => scrollToBottom() })
6398
6495
  })
6399
6496
  ] })
6400
6497
  });
6401
6498
  };
6402
- _CopilotChatView.ScrollView = ({ children, autoScroll = true, scrollToBottomButton, feather, inputContainerHeight = 0, isResizing = false, className, ...props }) => {
6499
+ const PinToSendScrollContainer = ({ children, scrollRef, contentRef, scrollToBottom, scrollToBottomButton, feather, inputContainerHeight, isResizing, nonAutoScrollEl, nonAutoScrollRefCallback, showScrollButton, className, ...props }) => {
6500
+ const spacerRef = (0, react.useRef)(null);
6501
+ usePinToSend({
6502
+ scrollRef,
6503
+ contentRef,
6504
+ spacerRef,
6505
+ topOffset: 16
6506
+ });
6507
+ const BoundFeather = renderSlot(feather, CopilotChatView.Feather, {});
6508
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScrollElementContext.Provider, {
6509
+ value: nonAutoScrollEl,
6510
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6511
+ className: cn("cpk:h-full cpk:max-h-full cpk:flex cpk:flex-col cpk:min-h-0 cpk:relative", className),
6512
+ children: [
6513
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6514
+ ref: nonAutoScrollRefCallback,
6515
+ className: "cpk:flex-1 cpk:min-h-0 cpk:overflow-y-auto cpk:overflow-x-hidden",
6516
+ ...props,
6517
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6518
+ ref: contentRef,
6519
+ className: "cpk:px-4 cpk:sm:px-0 cpk:[div[data-sidebar-chat]_&]:px-8 cpk:[div[data-popup-chat]_&]:px-6",
6520
+ children
6521
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6522
+ ref: spacerRef,
6523
+ "data-pin-to-send-spacer": true,
6524
+ "aria-hidden": "true",
6525
+ style: {
6526
+ height: 0,
6527
+ flex: "0 0 auto"
6528
+ }
6529
+ })]
6530
+ }),
6531
+ BoundFeather,
6532
+ showScrollButton && !isResizing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6533
+ className: "cpk:absolute cpk:inset-x-0 cpk:flex cpk:justify-center cpk:z-30 cpk:pointer-events-none",
6534
+ style: { bottom: `${inputContainerHeight + SCROLL_BUTTON_OFFSET}px` },
6535
+ children: renderSlot(scrollToBottomButton, CopilotChatView.ScrollToBottomButton, { onClick: () => scrollToBottom() })
6536
+ })
6537
+ ]
6538
+ })
6539
+ });
6540
+ };
6541
+ _CopilotChatView.ScrollView = ({ children, autoScroll = "pin-to-bottom", scrollToBottomButton, feather, inputContainerHeight = 0, isResizing = false, className, ...props }) => {
6542
+ const mode = normalizeAutoScroll(autoScroll);
6403
6543
  const [hasMounted, setHasMounted] = (0, react.useState)(false);
6404
- const { scrollRef, contentRef, scrollToBottom } = (0, use_stick_to_bottom.useStickToBottom)();
6544
+ const scrollRef = (0, react.useRef)(null);
6545
+ const contentRef = (0, react.useRef)(null);
6546
+ const scrollToBottom = (0, react.useCallback)(() => {
6547
+ const el = scrollRef.current;
6548
+ if (el) el.scrollTo({
6549
+ top: el.scrollHeight,
6550
+ behavior: "smooth"
6551
+ });
6552
+ }, []);
6405
6553
  const [showScrollButton, setShowScrollButton] = (0, react.useState)(false);
6406
6554
  const [nonAutoScrollEl, setNonAutoScrollEl] = (0, react.useState)(null);
6407
6555
  const nonAutoScrollRefCallback = (0, react.useCallback)((el) => {
@@ -6412,7 +6560,7 @@ function CopilotChatView({ messageView, input, scrollView, suggestionView, welco
6412
6560
  setHasMounted(true);
6413
6561
  }, []);
6414
6562
  (0, react.useEffect)(() => {
6415
- if (autoScroll) return;
6563
+ if (mode === "pin-to-bottom") return;
6416
6564
  const scrollElement = scrollRef.current;
6417
6565
  if (!scrollElement) return;
6418
6566
  const checkScroll = () => {
@@ -6426,7 +6574,7 @@ function CopilotChatView({ messageView, input, scrollView, suggestionView, welco
6426
6574
  scrollElement.removeEventListener("scroll", checkScroll);
6427
6575
  resizeObserver.disconnect();
6428
6576
  };
6429
- }, [scrollRef, autoScroll]);
6577
+ }, [scrollRef, mode]);
6430
6578
  if (!hasMounted) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6431
6579
  className: "cpk:h-full cpk:max-h-full cpk:flex cpk:flex-col cpk:min-h-0 cpk:overflow-y-auto cpk:overflow-x-hidden",
6432
6580
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -6434,7 +6582,7 @@ function CopilotChatView({ messageView, input, scrollView, suggestionView, welco
6434
6582
  children
6435
6583
  })
6436
6584
  });
6437
- if (!autoScroll) {
6585
+ if (mode === "none") {
6438
6586
  const BoundFeather = renderSlot(feather, CopilotChatView.Feather, {});
6439
6587
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScrollElementContext.Provider, {
6440
6588
  value: nonAutoScrollEl,
@@ -6451,13 +6599,28 @@ function CopilotChatView({ messageView, input, scrollView, suggestionView, welco
6451
6599
  BoundFeather,
6452
6600
  showScrollButton && !isResizing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6453
6601
  className: "cpk:absolute cpk:inset-x-0 cpk:flex cpk:justify-center cpk:z-30 cpk:pointer-events-none",
6454
- style: { bottom: `${inputContainerHeight + FEATHER_HEIGHT + 16}px` },
6602
+ style: { bottom: `${inputContainerHeight + SCROLL_BUTTON_OFFSET}px` },
6455
6603
  children: renderSlot(scrollToBottomButton, CopilotChatView.ScrollToBottomButton, { onClick: () => scrollToBottom() })
6456
6604
  })
6457
6605
  ]
6458
6606
  })
6459
6607
  });
6460
6608
  }
6609
+ if (mode === "pin-to-send") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PinToSendScrollContainer, {
6610
+ scrollRef,
6611
+ contentRef,
6612
+ scrollToBottom,
6613
+ scrollToBottomButton,
6614
+ feather,
6615
+ inputContainerHeight,
6616
+ isResizing,
6617
+ nonAutoScrollEl,
6618
+ nonAutoScrollRefCallback,
6619
+ showScrollButton,
6620
+ className,
6621
+ ...props,
6622
+ children
6623
+ });
6461
6624
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(use_stick_to_bottom.StickToBottom, {
6462
6625
  className: cn("cpk:flex-1 cpk:max-h-full cpk:flex cpk:flex-col cpk:min-h-0", className),
6463
6626
  resize: "smooth",
@@ -6480,9 +6643,8 @@ function CopilotChatView({ messageView, input, scrollView, suggestionView, welco
6480
6643
  ...props,
6481
6644
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDown, { className: "cpk:w-4 cpk:h-4 cpk:text-gray-600 cpk:dark:text-white" })
6482
6645
  });
6483
- _CopilotChatView.Feather = ({ className, style, ...props }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6484
- className: cn("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", "cpk:from-white cpk:via-white cpk:to-transparent", "cpk:dark:from-[rgb(33,33,33)] cpk:dark:via-[rgb(33,33,33)]", className),
6485
- style,
6646
+ _CopilotChatView.Feather = ({ className, ...props }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6647
+ className,
6486
6648
  ...props
6487
6649
  });
6488
6650
  _CopilotChatView.WelcomeMessage = ({ className, ...props }) => {
@@ -6650,7 +6812,9 @@ async function transcribeAudio(core, audioBlob, filename = "recording.webm") {
6650
6812
  function CopilotChat({ agentId, threadId, labels, chatView, isModalDefaultOpen, attachments: attachmentsConfig, onError, throttleMs, ...props }) {
6651
6813
  const existingConfig = useCopilotChatConfiguration();
6652
6814
  const resolvedAgentId = agentId ?? existingConfig?.agentId ?? _copilotkit_shared.DEFAULT_AGENT_ID;
6653
- const resolvedThreadId = (0, react.useMemo)(() => threadId ?? existingConfig?.threadId ?? (0, _copilotkit_shared.randomUUID)(), [threadId, existingConfig?.threadId]);
6815
+ const providedThreadId = threadId ?? existingConfig?.threadId;
6816
+ const resolvedThreadId = (0, react.useMemo)(() => providedThreadId ?? (0, _copilotkit_shared.randomUUID)(), [providedThreadId]);
6817
+ const hasExplicitThreadId = !!threadId || !!existingConfig?.hasExplicitThreadId;
6654
6818
  const { agent } = useAgent({
6655
6819
  agentId: resolvedAgentId,
6656
6820
  threadId: resolvedThreadId,
@@ -6688,7 +6852,10 @@ function CopilotChat({ agentId, threadId, labels, chatView, isModalDefaultOpen,
6688
6852
  const isTranscriptionEnabled = copilotkit.audioFileTranscriptionEnabled;
6689
6853
  const isMediaRecorderSupported = typeof window !== "undefined" && typeof MediaRecorder !== "undefined";
6690
6854
  const { messageView: providedMessageView, suggestionView: providedSuggestionView, onStop: providedStopHandler, ...restProps } = props;
6855
+ const [lastConnectedThreadId, setLastConnectedThreadId] = (0, react.useState)(null);
6856
+ const isConnecting = hasExplicitThreadId && lastConnectedThreadId !== resolvedThreadId;
6691
6857
  (0, react.useEffect)(() => {
6858
+ if (!hasExplicitThreadId) return;
6692
6859
  let detached = false;
6693
6860
  const connectAbortController = new AbortController();
6694
6861
  if (agent instanceof _ag_ui_client.HttpAgent) agent.abortController = connectAbortController;
@@ -6698,6 +6865,10 @@ function CopilotChat({ agentId, threadId, labels, chatView, isModalDefaultOpen,
6698
6865
  } catch (error) {
6699
6866
  if (detached) return;
6700
6867
  console.error("CopilotChat: connectAgent failed", error);
6868
+ } finally {
6869
+ if (!detached) (typeof requestAnimationFrame === "function" ? requestAnimationFrame : (cb) => setTimeout(cb, 16))(() => {
6870
+ if (!detached) setLastConnectedThreadId(resolvedThreadId);
6871
+ });
6701
6872
  }
6702
6873
  };
6703
6874
  connect(agent);
@@ -6709,7 +6880,8 @@ function CopilotChat({ agentId, threadId, labels, chatView, isModalDefaultOpen,
6709
6880
  }, [
6710
6881
  resolvedThreadId,
6711
6882
  agent,
6712
- resolvedAgentId
6883
+ resolvedAgentId,
6884
+ hasExplicitThreadId
6713
6885
  ]);
6714
6886
  const onSubmitInput = (0, react.useCallback)(async (value) => {
6715
6887
  if (selectedAttachments.some((a) => a.status === "uploading")) {
@@ -6862,6 +7034,22 @@ function CopilotChat({ agentId, threadId, labels, chatView, isModalDefaultOpen,
6862
7034
  const toolCallsKey = "toolCalls" in m && Array.isArray(m.toolCalls) ? m.toolCalls.map((tc) => `${tc.id}:${tc.function?.arguments?.length ?? 0}`).join(";") : "";
6863
7035
  return `${m.id}:${m.role}:${contentKey}:${toolCallsKey}`;
6864
7036
  }).join(",")]);
7037
+ const lastUserMessageId = (0, react.useMemo)(() => {
7038
+ for (let i = messages.length - 1; i >= 0; i--) if (messages[i].role === "user") return messages[i].id;
7039
+ return null;
7040
+ }, [messages]);
7041
+ const [sendNonce, setSendNonce] = (0, react.useState)(0);
7042
+ const prevLastUserMessageIdRef = (0, react.useRef)(lastUserMessageId);
7043
+ (0, react.useEffect)(() => {
7044
+ if (lastUserMessageId && lastUserMessageId !== prevLastUserMessageIdRef.current) {
7045
+ setSendNonce((n) => n + 1);
7046
+ prevLastUserMessageIdRef.current = lastUserMessageId;
7047
+ }
7048
+ }, [lastUserMessageId]);
7049
+ const lastUserMessageState = (0, react.useMemo)(() => ({
7050
+ id: lastUserMessageId,
7051
+ sendNonce
7052
+ }), [lastUserMessageId, sendNonce]);
6865
7053
  const RenderedChatView = renderSlot(chatView, CopilotChatView, {
6866
7054
  ...mergedProps,
6867
7055
  messages,
@@ -6880,11 +7068,14 @@ function CopilotChat({ agentId, threadId, labels, chatView, isModalDefaultOpen,
6880
7068
  dragOver,
6881
7069
  onDragOver: handleDragOver,
6882
7070
  onDragLeave: handleDragLeave,
6883
- onDrop: handleDrop
7071
+ onDrop: handleDrop,
7072
+ isConnecting,
7073
+ hasExplicitThreadId
6884
7074
  });
6885
7075
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopilotChatConfigurationProvider, {
6886
7076
  agentId: resolvedAgentId,
6887
7077
  threadId: resolvedThreadId,
7078
+ hasExplicitThreadId,
6888
7079
  labels,
6889
7080
  isModalDefaultOpen,
6890
7081
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -6915,7 +7106,10 @@ function CopilotChat({ agentId, threadId, labels, chatView, isModalDefaultOpen,
6915
7106
  },
6916
7107
  children: transcriptionError
6917
7108
  }),
6918
- RenderedChatView
7109
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LastUserMessageContext.Provider, {
7110
+ value: lastUserMessageState,
7111
+ children: RenderedChatView
7112
+ })
6919
7113
  ]
6920
7114
  })
6921
7115
  });
@@ -8641,12 +8835,19 @@ function useCoAgentStateRenders() {
8641
8835
  //#region src/context/threads-context.tsx
8642
8836
  const ThreadsContext = (0, react.createContext)(void 0);
8643
8837
  function ThreadsProvider({ children, threadId: explicitThreadId }) {
8644
- const [internalThreadId, setThreadId] = (0, react.useState)(() => (0, _copilotkit_shared.randomUUID)());
8838
+ const [internalThreadId, setInternalThreadId] = (0, react.useState)(() => (0, _copilotkit_shared.randomUUID)());
8839
+ const [internalIsExplicit, setInternalIsExplicit] = (0, react.useState)(false);
8645
8840
  const threadId = explicitThreadId ?? internalThreadId;
8841
+ const isThreadIdExplicit = explicitThreadId != null || internalIsExplicit;
8842
+ const setThreadId = (0, react.useCallback)((value) => {
8843
+ setInternalThreadId(value);
8844
+ setInternalIsExplicit(true);
8845
+ }, []);
8646
8846
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThreadsContext.Provider, {
8647
8847
  value: {
8648
8848
  threadId,
8649
- setThreadId
8849
+ setThreadId,
8850
+ isThreadIdExplicit
8650
8851
  },
8651
8852
  children
8652
8853
  });
@@ -9348,7 +9549,7 @@ function CopilotKitInternal(cpkProps) {
9348
9549
  if (props.agent) setAgentSession({ agentName: props.agent });
9349
9550
  else setAgentSession(null);
9350
9551
  }, [props.agent]);
9351
- const { threadId, setThreadId: setInternalThreadId } = useThreads();
9552
+ const { threadId, setThreadId: setInternalThreadId, isThreadIdExplicit } = useThreads();
9352
9553
  const setThreadId = (0, react.useCallback)((value) => {
9353
9554
  if (props.threadId) throw new Error("Cannot call setThreadId() when threadId is provided via props.");
9354
9555
  setInternalThreadId(value);
@@ -9548,6 +9749,7 @@ function CopilotKitInternal(cpkProps) {
9548
9749
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopilotChatConfigurationProvider, {
9549
9750
  agentId: props.agent ?? "default",
9550
9751
  threadId,
9752
+ hasExplicitThreadId: isThreadIdExplicit,
9551
9753
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CopilotContext.Provider, {
9552
9754
  value: copilotContextValue,
9553
9755
  children: [
@@ -10017,4 +10219,4 @@ Object.defineProperty(exports, 'useToast', {
10017
10219
  return useToast;
10018
10220
  }
10019
10221
  });
10020
- //# sourceMappingURL=copilotkit-CSJw5BG8.cjs.map
10222
+ //# sourceMappingURL=copilotkit-BAkj3zUc.cjs.map