@copilotkit/react-core 1.79.1 → 1.80.0

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 (42) hide show
  1. package/dist/{copilotkit-CD3uPUN3.mjs → copilotkit-BNLtgOyD.mjs} +1855 -221
  2. package/dist/copilotkit-BNLtgOyD.mjs.map +1 -0
  3. package/dist/{copilotkit-Yz8qXx00.d.mts → copilotkit-CWOSXhRO.d.mts} +277 -40
  4. package/dist/copilotkit-CWOSXhRO.d.mts.map +1 -0
  5. package/dist/{copilotkit-DEaA0Cat.cjs → copilotkit-DKICi3nC.cjs} +1843 -196
  6. package/dist/copilotkit-DKICi3nC.cjs.map +1 -0
  7. package/dist/{copilotkit-k7IYk2AN.d.cts → copilotkit-Dfw9HzO9.d.cts} +277 -40
  8. package/dist/copilotkit-Dfw9HzO9.d.cts.map +1 -0
  9. package/dist/index.cjs +15 -3
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +1 -1
  12. package/dist/index.d.cts.map +1 -1
  13. package/dist/index.d.mts +1 -1
  14. package/dist/index.d.mts.map +1 -1
  15. package/dist/index.mjs +15 -3
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/index.umd.js +127 -16
  18. package/dist/index.umd.js.map +1 -1
  19. package/dist/v2/context.d.cts +7 -0
  20. package/dist/v2/context.d.cts.map +1 -1
  21. package/dist/v2/context.d.mts +7 -0
  22. package/dist/v2/context.d.mts.map +1 -1
  23. package/dist/v2/headless.cjs +106 -16
  24. package/dist/v2/headless.cjs.map +1 -1
  25. package/dist/v2/headless.d.cts +20 -2
  26. package/dist/v2/headless.d.cts.map +1 -1
  27. package/dist/v2/headless.d.mts +20 -2
  28. package/dist/v2/headless.d.mts.map +1 -1
  29. package/dist/v2/headless.mjs +106 -16
  30. package/dist/v2/headless.mjs.map +1 -1
  31. package/dist/v2/index.cjs +3 -1
  32. package/dist/v2/index.css +1 -1
  33. package/dist/v2/index.d.cts +2 -2
  34. package/dist/v2/index.d.mts +2 -2
  35. package/dist/v2/index.mjs +2 -2
  36. package/dist/v2/index.umd.js +1858 -222
  37. package/dist/v2/index.umd.js.map +1 -1
  38. package/package.json +11 -8
  39. package/dist/copilotkit-CD3uPUN3.mjs.map +0 -1
  40. package/dist/copilotkit-DEaA0Cat.cjs.map +0 -1
  41. package/dist/copilotkit-Yz8qXx00.d.mts.map +0 -1
  42. package/dist/copilotkit-k7IYk2AN.d.cts.map +0 -1
@@ -47,6 +47,8 @@ let zod = require("zod");
47
47
  let _copilotkit_mcp_apps_renderer_activity = require("@copilotkit/mcp-apps-renderer/activity");
48
48
  let _copilotkit_a2ui_renderer = require("@copilotkit/a2ui-renderer");
49
49
  let zod_to_json_schema = require("zod-to-json-schema");
50
+ let _radix_ui_react_popover = require("@radix-ui/react-popover");
51
+ _radix_ui_react_popover = __toESM(_radix_ui_react_popover);
50
52
  let react_dom = require("react-dom");
51
53
  let _tanstack_react_virtual = require("@tanstack/react-virtual");
52
54
  let use_stick_to_bottom = require("use-stick-to-bottom");
@@ -69,7 +71,7 @@ function shallowEqual(obj1, obj2) {
69
71
  * Returns true only for plain JS objects (`{}`), excluding arrays, Dates,
70
72
  * class instances, and other exotic objects that happen to have typeof "object".
71
73
  */
72
- function isPlainObject(obj) {
74
+ function isPlainObject$1(obj) {
73
75
  return obj !== null && typeof obj === "object" && Object.prototype.toString.call(obj) === "[object Object]";
74
76
  }
75
77
  /**
@@ -88,7 +90,7 @@ function isPlainObject(obj) {
88
90
  function useShallowStableRef(value) {
89
91
  const ref = (0, react.useRef)(value);
90
92
  if (ref.current === value) return ref.current;
91
- if (isPlainObject(ref.current) && isPlainObject(value)) {
93
+ if (isPlainObject$1(ref.current) && isPlainObject$1(value)) {
92
94
  if (shallowEqual(ref.current, value)) return ref.current;
93
95
  }
94
96
  ref.current = value;
@@ -104,6 +106,8 @@ const CopilotChatDefaultLabels = {
104
106
  chatInputToolbarFinishTranscribeButtonLabel: "Finish",
105
107
  chatInputToolbarAddButtonLabel: "Add attachments",
106
108
  chatInputToolbarToolsButtonLabel: "Tools",
109
+ chatInputToolbarSendButtonLabel: "Send message",
110
+ chatInputToolbarStopButtonLabel: "Stop",
107
111
  assistantMessageToolbarCopyCodeLabel: "Copy",
108
112
  assistantMessageToolbarCopyCodeCopiedLabel: "Copied",
109
113
  assistantMessageToolbarCopyMessageLabel: "Copy",
@@ -116,13 +120,18 @@ const CopilotChatDefaultLabels = {
116
120
  assistantMessageToolbarInspectorHideDescription: "Until you reload this page",
117
121
  assistantMessageToolbarThumbsUpLabel: "Good response",
118
122
  assistantMessageToolbarThumbsDownLabel: "Bad response",
123
+ assistantMessageFeedbackCommentPlaceholder: "Add a comment (optional)",
124
+ assistantMessageFeedbackCommentSubmitLabel: "Send comment",
119
125
  assistantMessageToolbarReadAloudLabel: "Read aloud",
120
126
  assistantMessageToolbarRegenerateLabel: "Regenerate",
121
127
  userMessageToolbarCopyMessageLabel: "Copy",
122
128
  userMessageToolbarEditMessageLabel: "Edit",
129
+ userMessageToolbarPreviousBranchLabel: "Previous branch",
130
+ userMessageToolbarNextBranchLabel: "Next branch",
123
131
  chatDisclaimerText: "AI can make mistakes. Please verify important information.",
124
132
  chatToggleOpenLabel: "Open chat",
125
133
  chatToggleCloseLabel: "Close chat",
134
+ chatScrollToBottomLabel: "Scroll to bottom",
126
135
  modalHeaderTitle: "CopilotKit Chat",
127
136
  welcomeMessageText: "How can I help you today?"
128
137
  };
@@ -384,9 +393,9 @@ ControlledModalOpenScope.displayName = "ControlledModalOpenScope";
384
393
 
385
394
  //#endregion
386
395
  //#region src/v2/lib/utils.ts
387
- const twMerge$10 = (0, tailwind_merge.extendTailwindMerge)({ prefix: "cpk" });
396
+ const twMerge$12 = (0, tailwind_merge.extendTailwindMerge)({ prefix: "cpk" });
388
397
  function cn(...inputs) {
389
- return twMerge$10((0, clsx.clsx)(inputs));
398
+ return twMerge$12((0, clsx.clsx)(inputs));
390
399
  }
391
400
 
392
401
  //#endregion
@@ -834,6 +843,11 @@ function renderSlot(slot, DefaultComponent, props) {
834
843
  //#region src/v2/components/chat/CopilotChatInput.tsx
835
844
  const SLASH_MENU_MAX_VISIBLE_ITEMS = 5;
836
845
  const SLASH_MENU_ITEM_HEIGHT_PX = 40;
846
+ /**
847
+ * The chat composer: a text area with the add-menu, voice and send/stop
848
+ * buttons, and a slash-command menu that lists `toolsMenu` items once the text
849
+ * starts with "/".
850
+ */
837
851
  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 }) {
838
852
  const isControlled = value !== void 0;
839
853
  const [internalValue, setInternalValue] = (0, react.useState)(() => value ?? "");
@@ -1053,10 +1067,12 @@ function CopilotChatInput({ mode = "input", onSubmitMessage, onStop, isRunning =
1053
1067
  send();
1054
1068
  };
1055
1069
  const BoundAudioRecorder = renderSlot(audioRecorder, CopilotChatAudioRecorder, { ref: audioRecorderRef });
1070
+ const showsStop = isProcessing && canStop;
1056
1071
  const BoundSendButton = renderSlot(sendButton, CopilotChatInput.SendButton, {
1057
1072
  onClick: handleSendButtonClick,
1058
1073
  disabled: isProcessing ? !canStop : !canSend,
1059
- children: isProcessing && canStop ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Square, { className: "cpk:size-[18px] cpk:fill-current" }) : void 0
1074
+ "aria-label": showsStop ? labels.chatInputToolbarStopButtonLabel : labels.chatInputToolbarSendButtonLabel,
1075
+ children: showsStop ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Square, { className: "cpk:size-[18px] cpk:fill-current" }) : void 0
1060
1076
  });
1061
1077
  const BoundStartTranscribeButton = renderSlot(startTranscribeButton, CopilotChatInput.StartTranscribeButton, { onClick: onStartTranscribe });
1062
1078
  const BoundCancelTranscribeButton = renderSlot(cancelTranscribeButton, CopilotChatInput.CancelTranscribeButton, { onClick: onCancelTranscribe });
@@ -1368,18 +1384,22 @@ function CopilotChatInput({ mode = "input", onSubmitMessage, onStop, isRunning =
1368
1384
  });
1369
1385
  }
1370
1386
  (function(_CopilotChatInput) {
1371
- _CopilotChatInput.SendButton = ({ className, children, ...props }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1372
- className: "cpk:mr-[10px]",
1373
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
1374
- type: "button",
1375
- "data-testid": "copilot-send-button",
1376
- variant: "chatInputToolbarPrimary",
1377
- size: "chatInputToolbarIcon",
1378
- className,
1379
- ...props,
1380
- children: children ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ArrowUp, { className: "cpk:size-[18px]" })
1381
- })
1382
- });
1387
+ _CopilotChatInput.SendButton = ({ className, children, ...props }) => {
1388
+ const labels = useCopilotChatConfiguration()?.labels ?? CopilotChatDefaultLabels;
1389
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1390
+ className: "cpk:mr-[10px]",
1391
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
1392
+ type: "button",
1393
+ "data-testid": "copilot-send-button",
1394
+ variant: "chatInputToolbarPrimary",
1395
+ size: "chatInputToolbarIcon",
1396
+ className,
1397
+ "aria-label": labels.chatInputToolbarSendButtonLabel,
1398
+ ...props,
1399
+ children: children ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ArrowUp, { className: "cpk:size-[18px]" })
1400
+ })
1401
+ });
1402
+ };
1383
1403
  const ToolbarButton = _CopilotChatInput.ToolbarButton = ({ icon, labelKey, defaultClassName, className, ...props }) => {
1384
1404
  const labels = useCopilotChatConfiguration()?.labels ?? CopilotChatDefaultLabels;
1385
1405
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
@@ -1389,6 +1409,7 @@ function CopilotChatInput({ mode = "input", onSubmitMessage, onStop, isRunning =
1389
1409
  variant: "chatInputToolbarSecondary",
1390
1410
  size: "chatInputToolbarIcon",
1391
1411
  className: (0, tailwind_merge.twMerge)(defaultClassName, className),
1412
+ "aria-label": labels[labelKey],
1392
1413
  ...props,
1393
1414
  children: icon
1394
1415
  })
@@ -1459,6 +1480,7 @@ function CopilotChatInput({ mode = "input", onSubmitMessage, onStop, isRunning =
1459
1480
  size: "chatInputToolbarIcon",
1460
1481
  className: (0, tailwind_merge.twMerge)("cpk:ml-1", className),
1461
1482
  disabled: isDisabled,
1483
+ "aria-label": labels.chatInputToolbarAddButtonLabel,
1462
1484
  ...props,
1463
1485
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "cpk:size-[20px]" })
1464
1486
  });
@@ -1608,6 +1630,32 @@ function warnAboutUnrenderedToolCalls(toolNames, registered, alreadyWarned) {
1608
1630
  }
1609
1631
  }
1610
1632
  /**
1633
+ * The tool-call renderers registered on the provider, kept current.
1634
+ * @internal
1635
+ */
1636
+ function useToolCallRenderers() {
1637
+ const { copilotkit } = (0, _copilotkit_react_core_v2_context.useCopilotKit)();
1638
+ return (0, react.useSyncExternalStore)((callback) => copilotkit.subscribe({ onRenderToolCallsChanged: callback }).unsubscribe, () => copilotkit.renderToolCalls, () => copilotkit.renderToolCalls);
1639
+ }
1640
+ /**
1641
+ * Which tool names have a renderer of their own, whether a wildcard (`"*"`)
1642
+ * renderer is registered, and which of them opted into grouping.
1643
+ * @internal
1644
+ */
1645
+ function useToolCallRendererNames() {
1646
+ const renderers = useToolCallRenderers();
1647
+ return (0, react.useMemo)(() => {
1648
+ const named = renderers.filter((rc) => rc.name !== "*");
1649
+ const wildcards = renderers.filter((rc) => rc.name === "*");
1650
+ return {
1651
+ named: new Set(named.map((rc) => rc.name)),
1652
+ groupedNames: new Set(named.filter((rc) => rc.group).map((rc) => rc.name)),
1653
+ wildcard: wildcards.length > 0,
1654
+ wildcardGroups: wildcards.some((rc) => rc.group)
1655
+ };
1656
+ }, [renderers]);
1657
+ }
1658
+ /**
1611
1659
  * Hook that returns a function to render tool calls based on the render functions
1612
1660
  * defined in CopilotKitProvider.
1613
1661
  *
@@ -1618,9 +1666,7 @@ function useRenderToolCall() {
1618
1666
  const agentId = useCopilotChatConfiguration()?.agentId ?? _copilotkit_shared.DEFAULT_AGENT_ID;
1619
1667
  const unrenderedToolNames = (0, react.useRef)(/* @__PURE__ */ new Set());
1620
1668
  const warnedToolNames = (0, react.useRef)(/* @__PURE__ */ new Set());
1621
- const renderToolCalls = (0, react.useSyncExternalStore)((callback) => {
1622
- return copilotkit.subscribe({ onRenderToolCallsChanged: callback }).unsubscribe;
1623
- }, () => copilotkit.renderToolCalls, () => copilotkit.renderToolCalls);
1669
+ const renderToolCalls = useToolCallRenderers();
1624
1670
  const renderToolCall = (0, react.useCallback)(({ toolCall, toolMessage }) => {
1625
1671
  const exactMatches = renderToolCalls.filter((rc) => rc.name === toolCall.function.name);
1626
1672
  const renderConfig = exactMatches.find((rc) => rc.agentId === agentId) || exactMatches.find((rc) => !rc.agentId) || exactMatches[0] || renderToolCalls.find((rc) => rc.name === "*");
@@ -1655,7 +1701,7 @@ function useRenderToolCall() {
1655
1701
 
1656
1702
  //#endregion
1657
1703
  //#region package.json
1658
- var version = "1.79.1";
1704
+ var version = "1.80.0";
1659
1705
 
1660
1706
  //#endregion
1661
1707
  //#region src/v2/components/CopilotKitInspector.tsx
@@ -1891,7 +1937,7 @@ function InlineFeatureWarning({ featureName }) {
1891
1937
  * Fetches resource content on-demand via proxied MCP requests. The React shell
1892
1938
  * owns the iframe; `bindMcpApp` owns the protocol.
1893
1939
  */
1894
- const MCPAppsActivityRenderer = function MCPAppsActivityRenderer({ content, message, agent }) {
1940
+ const MCPAppsActivityRenderer = function MCPAppsActivityRenderer({ content, message, agent, allowSameOriginFallback }) {
1895
1941
  const { copilotkit } = (0, _copilotkit_react_core_v2_context.useCopilotKit)();
1896
1942
  const messageId = message?.id;
1897
1943
  const containerRef = (0, react.useRef)(null);
@@ -1941,6 +1987,7 @@ const MCPAppsActivityRenderer = function MCPAppsActivityRenderer({ content, mess
1941
1987
  getAgent: () => agentRef.current,
1942
1988
  host: copilotkit,
1943
1989
  messageId,
1990
+ options: { allowSameOriginFallback },
1944
1991
  hooks: {
1945
1992
  onResource: (resource) => {
1946
1993
  if (!mounted) return;
@@ -1982,6 +2029,7 @@ const MCPAppsActivityRenderer = function MCPAppsActivityRenderer({ content, mess
1982
2029
  agent,
1983
2030
  copilotkit,
1984
2031
  messageId,
2032
+ allowSameOriginFallback,
1985
2033
  content.resourceUri,
1986
2034
  content.serverHash,
1987
2035
  content.serverId
@@ -3196,7 +3244,8 @@ function defineToolCallRenderer(def) {
3196
3244
  name: def.name,
3197
3245
  args: argsSchema,
3198
3246
  render: def.render,
3199
- ...def.agentId ? { agentId: def.agentId } : {}
3247
+ ...def.agentId ? { agentId: def.agentId } : {},
3248
+ ...def.group ? { group: true } : {}
3200
3249
  };
3201
3250
  }
3202
3251
 
@@ -3412,6 +3461,11 @@ const DEFAULT_DESIGN_SKILL = `When generating UI with generateSandboxedUi, follo
3412
3461
  - Use CSS Grid or Flexbox for layout. Ensure the UI looks good at any width.
3413
3462
  - Minimal transitions (150ms) for hover/focus states only. No decorative animations.
3414
3463
  - Keep the UI focused and dense — avoid excessive padding. Use compact spacing (8–12px gaps, 10–14px padding in controls).`;
3464
+ /** The MCP Apps renderer with `mcpApps.allowSameOriginFallback` applied. */
3465
+ const SameOriginMCPAppsActivityRenderer = (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MCPAppsActivityRenderer, {
3466
+ ...props,
3467
+ allowSameOriginFallback: true
3468
+ });
3415
3469
  const DEFAULT_LEARNING = Object.freeze({});
3416
3470
  function toLearningConfig(learning) {
3417
3471
  if (learning === true) return DEFAULT_LEARNING;
@@ -3428,7 +3482,8 @@ function useStableArrayProp(prop, warningMessage, isMeaningfulChange) {
3428
3482
  }, [value, warningMessage]);
3429
3483
  return value;
3430
3484
  }
3431
- const CopilotKitProvider = ({ children, runtimeUrl, headers: headersProp = EMPTY_HEADERS, credentials, messageFilter, publicApiKey, publicLicenseKey, licenseToken, properties = EMPTY_PROPERTIES, agents__unsafe_dev_only: agents = EMPTY_AGENTS, selfManagedAgents = EMPTY_AGENTS, renderToolCalls, renderActivityMessages, renderCustomMessages, frontendTools, humanInTheLoop, openGenerativeUI, enableInspector, agentId, showIntelligenceIndicator = true, useSingleEndpoint, onError, a2ui, defaultThrottleMs, debug, learning }) => {
3485
+ /** Provide a shared core instance and synchronize runtime, tool, and UI configuration. */
3486
+ const CopilotKitProvider = ({ children, runtimeUrl, headers: headersProp = EMPTY_HEADERS, credentials, messageFilter, publicApiKey, publicLicenseKey, licenseToken, properties = EMPTY_PROPERTIES, agents__unsafe_dev_only: agents = EMPTY_AGENTS, selfManagedAgents = EMPTY_AGENTS, renderToolCalls, renderActivityMessages, renderCustomMessages, frontendTools, humanInTheLoop, openGenerativeUI, enableInspector, agentId, showIntelligenceIndicator = true, useSingleEndpoint, onError, a2ui, mcpApps, defaultThrottleMs, debug, learning }) => {
3432
3487
  const learningConfig = (0, react.useMemo)(() => toLearningConfig(learning), [learning]);
3433
3488
  const [shouldRenderInspector, setShouldRenderInspector] = (0, react.useState)(false);
3434
3489
  const [inspectorVisible, setInspectorVisible] = (0, react.useState)(false);
@@ -3485,11 +3540,12 @@ const CopilotKitProvider = ({ children, runtimeUrl, headers: headersProp = EMPTY
3485
3540
  const core = copilotkitRef.current;
3486
3541
  return (0, _copilotkit_a2ui_renderer.filterCatalog)(rawCatalog, (name) => core ? core.isCatalogComponentEnabled(name) : true);
3487
3542
  }, [rawCatalog, catalogToggleVersion]);
3543
+ const mcpAppsAllowSameOrigin = mcpApps?.allowSameOriginFallback === true;
3488
3544
  const builtInActivityRenderers = (0, react.useMemo)(() => {
3489
3545
  const renderers = [{
3490
3546
  activityType: _copilotkit_mcp_apps_renderer_activity.MCPAppsActivityType,
3491
3547
  content: _copilotkit_mcp_apps_renderer_activity.MCPAppsActivityContentSchema,
3492
- render: MCPAppsActivityRenderer
3548
+ render: mcpAppsAllowSameOrigin ? SameOriginMCPAppsActivityRenderer : MCPAppsActivityRenderer
3493
3549
  }];
3494
3550
  if (openGenUIActive) renderers.push({
3495
3551
  activityType: OpenGenerativeUIActivityType,
@@ -3507,7 +3563,8 @@ const CopilotKitProvider = ({ children, runtimeUrl, headers: headersProp = EMPTY
3507
3563
  a2uiActive,
3508
3564
  openGenUIActive,
3509
3565
  a2ui,
3510
- filteredCatalog
3566
+ filteredCatalog,
3567
+ mcpAppsAllowSameOrigin
3511
3568
  ]);
3512
3569
  const allActivityRenderers = (0, react.useMemo)(() => {
3513
3570
  return [...renderActivityMessagesList, ...builtInActivityRenderers];
@@ -3615,6 +3672,7 @@ const CopilotKitProvider = ({ children, runtimeUrl, headers: headersProp = EMPTY
3615
3672
  description: _copilotkit_shared.GENERATE_SANDBOXED_UI_DESCRIPTION,
3616
3673
  parameters: GenerateSandboxedUiArgsSchema,
3617
3674
  handler: async () => "UI generated",
3675
+ reconnectBehavior: "resume-pending",
3618
3676
  followUp: true,
3619
3677
  render: OpenGenerativeUIToolRenderer
3620
3678
  }];
@@ -4020,12 +4078,86 @@ function useRenderActivityMessage() {
4020
4078
  }), [renderActivityMessage, findRenderer]);
4021
4079
  }
4022
4080
 
4081
+ //#endregion
4082
+ //#region src/v2/hooks/tool-registration.ts
4083
+ /**
4084
+ * Deps comparison shared by `useFrontendTool` (and through it
4085
+ * `useHumanInTheLoop` and `useComponent`) and `useRenderTool`. Not part of the
4086
+ * public API.
4087
+ */
4088
+ const EMPTY_DEPS$1 = [];
4089
+ function isPlainObject(value) {
4090
+ const proto = Object.getPrototypeOf(value);
4091
+ return proto === Object.prototype || proto === null;
4092
+ }
4093
+ /**
4094
+ * Plain data (primitives, plain objects, arrays, dates) compares by value.
4095
+ * Functions always compare equal: these hooks used to compare
4096
+ * `JSON.stringify(deps)`, which turns every function into `null`, and a
4097
+ * function that counted as changed would re-register on every render when it
4098
+ * is written inline (and loop in a component that also reads
4099
+ * `useRenderToolCall`). Everything else (class instances, maps, sets) compares
4100
+ * by identity. A cycle inside plain data is safe: a pair already being
4101
+ * compared counts as equal for that branch.
4102
+ */
4103
+ function depEqual(a, b, seen) {
4104
+ if (Object.is(a, b)) return true;
4105
+ if (typeof a === "function" && typeof b === "function") return true;
4106
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
4107
+ if (a instanceof Date && b instanceof Date) return Object.is(a.getTime(), b.getTime());
4108
+ const aIsArray = Array.isArray(a);
4109
+ if (aIsArray !== Array.isArray(b)) return false;
4110
+ if (!aIsArray && !(isPlainObject(a) && isPlainObject(b))) return false;
4111
+ if (seen.get(a) === b) return true;
4112
+ seen.set(a, b);
4113
+ if (aIsArray) {
4114
+ const left = a;
4115
+ const right = b;
4116
+ if (left.length !== right.length) return false;
4117
+ for (let i = 0; i < left.length; i++) if (!depEqual(left[i], right[i], seen)) return false;
4118
+ return true;
4119
+ }
4120
+ const left = a;
4121
+ const right = b;
4122
+ const leftKeys = Object.keys(left);
4123
+ if (leftKeys.length !== Object.keys(right).length) return false;
4124
+ for (const key of leftKeys) {
4125
+ if (!Object.prototype.hasOwnProperty.call(right, key)) return false;
4126
+ if (!depEqual(left[key], right[key], seen)) return false;
4127
+ }
4128
+ return true;
4129
+ }
4130
+ /** Whether two deps arrays are equal. A change in length is always a change. */
4131
+ function depsEqual(prev, next) {
4132
+ if (prev.length !== next.length) return false;
4133
+ const seen = /* @__PURE__ */ new Map();
4134
+ for (let i = 0; i < prev.length; i++) if (!depEqual(prev[i], next[i], seen)) return false;
4135
+ return true;
4136
+ }
4137
+ /**
4138
+ * Turns a caller's `deps` array into a number that changes exactly when the
4139
+ * deps change, by the rules of {@link depsEqual}. Put the number, not the
4140
+ * array, in the registration effect's dependency list: that keeps the list a
4141
+ * constant length and never serializes a value.
4142
+ */
4143
+ function useDepsVersion(deps) {
4144
+ const next = deps ?? EMPTY_DEPS$1;
4145
+ const ref = (0, react.useRef)({
4146
+ deps: next,
4147
+ version: 0
4148
+ });
4149
+ if (ref.current.deps !== next && !depsEqual(ref.current.deps, next)) ref.current = {
4150
+ deps: next,
4151
+ version: ref.current.version + 1
4152
+ };
4153
+ return ref.current.version;
4154
+ }
4155
+
4023
4156
  //#endregion
4024
4157
  //#region src/v2/hooks/use-frontend-tool.tsx
4025
- const EMPTY_DEPS$2 = [];
4026
4158
  function useFrontendTool(tool, deps) {
4027
4159
  const { copilotkit } = (0, _copilotkit_react_core_v2_context.useCopilotKit)();
4028
- const extraDeps = deps ?? EMPTY_DEPS$2;
4160
+ const depsVersion = useDepsVersion(deps);
4029
4161
  (0, react.useLayoutEffect)(() => {
4030
4162
  const name = tool.name;
4031
4163
  if (copilotkit.getTool({
@@ -4049,14 +4181,14 @@ function useFrontendTool(tool, deps) {
4049
4181
  tool.name,
4050
4182
  tool.available,
4051
4183
  copilotkit,
4052
- JSON.stringify(extraDeps),
4184
+ depsVersion,
4053
4185
  JSON.stringify(tool.webmcp ?? null)
4054
4186
  ]);
4055
4187
  }
4056
4188
 
4057
4189
  //#endregion
4058
4190
  //#region src/v2/hooks/use-frontend-tools.tsx
4059
- const EMPTY_DEPS$1 = [];
4191
+ const EMPTY_DEPS = [];
4060
4192
  const EMPTY_TOOLS = [];
4061
4193
  /**
4062
4194
  * Register a list of frontend tools whose length can change between renders.
@@ -4110,7 +4242,7 @@ const EMPTY_TOOLS = [];
4110
4242
  function useFrontendTools(tools, deps) {
4111
4243
  const { copilotkit } = (0, _copilotkit_react_core_v2_context.useCopilotKit)();
4112
4244
  const toolList = tools ?? EMPTY_TOOLS;
4113
- const extraDeps = deps ?? EMPTY_DEPS$1;
4245
+ const extraDeps = deps ?? EMPTY_DEPS;
4114
4246
  (0, react.useLayoutEffect)(() => {
4115
4247
  const deduped = /* @__PURE__ */ new Map();
4116
4248
  for (const tool of toolList) {
@@ -4221,7 +4353,6 @@ function useComponent(config, deps) {
4221
4353
 
4222
4354
  //#endregion
4223
4355
  //#region src/v2/hooks/use-render-tool.tsx
4224
- const EMPTY_DEPS = [];
4225
4356
  /**
4226
4357
  * Registers a renderer entry in CopilotKit's `renderToolCalls` registry.
4227
4358
  *
@@ -4265,7 +4396,7 @@ const EMPTY_DEPS = [];
4265
4396
  */
4266
4397
  function useRenderTool(config, deps) {
4267
4398
  const { copilotkit } = (0, _copilotkit_react_core_v2_context.useCopilotKit)();
4268
- const extraDeps = deps ?? EMPTY_DEPS;
4399
+ const depsVersion = useDepsVersion(deps);
4269
4400
  (0, react.useEffect)(() => {
4270
4401
  const renderer = config.name === "*" && !config.parameters ? defineToolCallRenderer({
4271
4402
  name: "*",
@@ -4273,7 +4404,8 @@ function useRenderTool(config, deps) {
4273
4404
  ...props,
4274
4405
  parameters: props.args
4275
4406
  }),
4276
- ...config.agentId ? { agentId: config.agentId } : {}
4407
+ ...config.agentId ? { agentId: config.agentId } : {},
4408
+ group: config.group
4277
4409
  }) : defineToolCallRenderer({
4278
4410
  name: config.name,
4279
4411
  args: config.parameters,
@@ -4291,16 +4423,395 @@ function useRenderTool(config, deps) {
4291
4423
  parameters: props.args
4292
4424
  });
4293
4425
  },
4294
- ...config.agentId ? { agentId: config.agentId } : {}
4426
+ ...config.agentId ? { agentId: config.agentId } : {},
4427
+ group: config.group
4295
4428
  });
4296
4429
  copilotkit.addHookRenderToolCall(renderer);
4297
4430
  }, [
4298
4431
  config.name,
4432
+ config.group,
4299
4433
  copilotkit,
4300
- JSON.stringify(extraDeps)
4434
+ depsVersion
4301
4435
  ]);
4302
4436
  }
4303
4437
 
4438
+ //#endregion
4439
+ //#region src/v2/components/chat/CopilotChatToolCallGroup.tsx
4440
+ /**
4441
+ * Set by a group around the calls it shows, so CopilotKit's built-in tool card
4442
+ * draws itself as a compact row that fits the group instead of a card.
4443
+ * @internal
4444
+ */
4445
+ const ToolCallGroupContext = (0, react.createContext)(null);
4446
+ /**
4447
+ * A call without a result is running only while the group is active: once
4448
+ * the run has moved on or ended, its result is never coming.
4449
+ */
4450
+ const entryStatus = ({ toolMessage }, active) => toolMessage ? toolMessage.error ? "failed" : "done" : active ? "running" : "stopped";
4451
+ /** @internal */
4452
+ const statusLabels = {
4453
+ running: "Running",
4454
+ done: "Done",
4455
+ failed: "Failed",
4456
+ stopped: "Stopped"
4457
+ };
4458
+ /** The first non-empty string argument: usually a command, path or query. */
4459
+ function argumentPreview(parameters) {
4460
+ if (!parameters || typeof parameters !== "object") return void 0;
4461
+ return Object.values(parameters).find((v) => typeof v === "string" && v.trim().length > 0)?.replace(/\s+/g, " ");
4462
+ }
4463
+ const entryPreview = (entry) => argumentPreview((0, _copilotkit_shared.partialJSONParse)(entry.toolCall.function.arguments));
4464
+ /** "run_command ×2, read_file" */
4465
+ function toolNames(calls) {
4466
+ const counts = /* @__PURE__ */ new Map();
4467
+ for (const { toolCall } of calls) {
4468
+ const name = toolCall.function.name;
4469
+ counts.set(name, (counts.get(name) ?? 0) + 1);
4470
+ }
4471
+ return [...counts].map(([name, count]) => count > 1 ? `${name} ×${count}` : name).join(", ");
4472
+ }
4473
+ const pluralize = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
4474
+ /**
4475
+ * Height transition between closed and its content's height. Closed content
4476
+ * stays mounted (so its state survives) but is inert.
4477
+ * @internal
4478
+ */
4479
+ function Collapsible({ open, id, className, children }) {
4480
+ const ref = (0, react.useRef)(null);
4481
+ (0, react.useLayoutEffect)(() => {
4482
+ ref.current?.toggleAttribute("inert", !open);
4483
+ }, [open]);
4484
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4485
+ ref,
4486
+ id,
4487
+ className: (0, tailwind_merge.twMerge)("cpk:grid cpk:transition-[grid-template-rows] cpk:duration-300 cpk:ease-[cubic-bezier(0.16,1,0.3,1)] cpk:motion-reduce:transition-none", className),
4488
+ style: { gridTemplateRows: open ? "1fr" : "0fr" },
4489
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4490
+ className: "cpk:min-h-0 cpk:overflow-hidden",
4491
+ children
4492
+ })
4493
+ });
4494
+ }
4495
+ function StatusIcon({ status, className }) {
4496
+ const Icon = {
4497
+ running: lucide_react.Loader2,
4498
+ done: lucide_react.Check,
4499
+ failed: lucide_react.X,
4500
+ stopped: lucide_react.Circle
4501
+ }[status];
4502
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
4503
+ "aria-hidden": "true",
4504
+ className: (0, tailwind_merge.twMerge)("cpk:size-3.5 cpk:shrink-0 cpk:text-muted-foreground", status === "running" && "cpk:animate-spin", status === "failed" && "cpk:text-destructive", className)
4505
+ });
4506
+ }
4507
+ function Chevron({ open, className }) {
4508
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, {
4509
+ "aria-hidden": "true",
4510
+ className: (0, tailwind_merge.twMerge)("cpk:size-3.5 cpk:shrink-0 cpk:transition-transform cpk:duration-200", open && "cpk:rotate-90", className)
4511
+ });
4512
+ }
4513
+ function FailedCount({ calls }) {
4514
+ const failed = calls.filter((call) => call.toolMessage?.error).length;
4515
+ if (!failed) return null;
4516
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4517
+ className: "cpk:shrink-0 cpk:text-destructive",
4518
+ children: [failed, " failed"]
4519
+ });
4520
+ }
4521
+ /**
4522
+ * Guarded JSON.stringify for the payload blocks. A circular reference would
4523
+ * otherwise crash the entire React tree on render.
4524
+ */
4525
+ function stringifyPayload(value) {
4526
+ if (typeof value === "string") return value;
4527
+ try {
4528
+ return JSON.stringify(value, null, 2);
4529
+ } catch (err) {
4530
+ console.warn("[CopilotKit] Failed to JSON.stringify tool-call payload; falling back to String():", err);
4531
+ try {
4532
+ return String(value);
4533
+ } catch {
4534
+ return "[unserializable]";
4535
+ }
4536
+ }
4537
+ }
4538
+ function Payload({ label, value }) {
4539
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4540
+ className: "cpk:text-[11px] cpk:font-medium cpk:uppercase cpk:tracking-wide cpk:text-muted-foreground",
4541
+ children: label
4542
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
4543
+ className: "cpk:m-0 cpk:mt-1.5 cpk:max-h-[200px] cpk:overflow-auto cpk:rounded-lg cpk:bg-muted cpk:p-2.5 cpk:font-mono cpk:text-xs cpk:leading-relaxed cpk:text-foreground cpk:whitespace-pre-wrap cpk:break-words",
4544
+ children: stringifyPayload(value)
4545
+ })] });
4546
+ }
4547
+ /**
4548
+ * A tool call's arguments and, once it has one, its result.
4549
+ * @internal
4550
+ */
4551
+ function ToolCallPayloads({ parameters, result, className }) {
4552
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4553
+ className: (0, tailwind_merge.twMerge)("cpk:grid cpk:gap-3", className),
4554
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Payload, {
4555
+ label: "Arguments",
4556
+ value: parameters ?? {}
4557
+ }), result !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Payload, {
4558
+ label: "Result",
4559
+ value: result
4560
+ })]
4561
+ });
4562
+ }
4563
+ /**
4564
+ * The built-in tool card drawn as a row inside a group: status, name and main
4565
+ * argument, opening to the payloads.
4566
+ * @internal
4567
+ */
4568
+ function ToolCallRow({ name, parameters, result, status, appearance, className, ...props }) {
4569
+ const [open, setOpen] = (0, react.useState)(false);
4570
+ const detailsId = (0, react.useId)();
4571
+ const preview = argumentPreview(parameters);
4572
+ const line = appearance === "line";
4573
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4574
+ "data-copilotkit": true,
4575
+ className: (0, tailwind_merge.twMerge)(!line && "cpk:border-t cpk:border-border", className),
4576
+ ...props,
4577
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4578
+ type: "button",
4579
+ "aria-expanded": open,
4580
+ "aria-controls": detailsId,
4581
+ onClick: () => setOpen((value) => !value),
4582
+ className: (0, tailwind_merge.twMerge)("cpk:group/row cpk:m-0 cpk:flex cpk:min-w-0 cpk:cursor-pointer cpk:items-center cpk:gap-2 cpk:border-none cpk:bg-transparent cpk:text-left cpk:text-[13px] cpk:text-inherit cpk:transition-colors cpk:hover:bg-accent/60 cpk:focus-visible:outline-2 cpk:focus-visible:-outline-offset-2 cpk:focus-visible:outline-ring", line ? "cpk:-mx-1.5 cpk:w-[calc(100%+0.75rem)] cpk:rounded-md cpk:px-1.5 cpk:py-1" : "cpk:w-full cpk:px-3.5 cpk:py-2"),
4583
+ children: [
4584
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusIcon, { status }),
4585
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4586
+ "data-testid": "copilot-tool-render-name",
4587
+ className: (0, tailwind_merge.twMerge)("cpk:shrink-0 cpk:font-mono cpk:text-[13px] cpk:font-medium cpk:text-foreground", status === "running" && "cpk-shimmer"),
4588
+ children: name
4589
+ }),
4590
+ preview && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4591
+ className: "cpk:min-w-0 cpk:truncate cpk:font-mono cpk:text-[13px] cpk:text-muted-foreground",
4592
+ children: preview
4593
+ }),
4594
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4595
+ "data-testid": "copilot-tool-render-status",
4596
+ className: "cpk:sr-only",
4597
+ children: statusLabels[status]
4598
+ }),
4599
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron, {
4600
+ open,
4601
+ className: (0, tailwind_merge.twMerge)("cpk:ml-auto cpk:text-muted-foreground", !open && "cpk:opacity-0 cpk:group-hover/row:opacity-100 cpk:group-focus-visible/row:opacity-100")
4602
+ })
4603
+ ]
4604
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Collapsible, {
4605
+ open,
4606
+ id: detailsId,
4607
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCallPayloads, {
4608
+ parameters,
4609
+ result,
4610
+ className: line ? "cpk:mt-1 cpk:mb-2 cpk:ml-5.5" : "cpk:px-3.5 cpk:pt-1 cpk:pb-3"
4611
+ })
4612
+ })]
4613
+ });
4614
+ }
4615
+ /**
4616
+ * Tool calls made in a row, collapsed into one summary. While a call runs the
4617
+ * summary names it ("Running read_file src/app.ts"); once they're all done it
4618
+ * reads "Used 3 tools" and opens to the calls.
4619
+ *
4620
+ * Each call is drawn by its registered renderer. CopilotKit's built-in card
4621
+ * (`useDefaultRenderTool`) draws a compact row inside the card and line
4622
+ * appearances; a custom wildcard renderer draws whatever it draws.
4623
+ */
4624
+ function CopilotChatToolCallGroup({ calls, renderCall, appearance = "card", isActive = false, className, ...props }) {
4625
+ const [open, setOpen] = (0, react.useState)(false);
4626
+ const bodyId = (0, react.useId)();
4627
+ const latest = calls[calls.length - 1];
4628
+ if (!latest) return null;
4629
+ const current = isActive ? calls.find((call) => !call.toolMessage) ?? latest : void 0;
4630
+ const preview = current ? entryPreview(current) : void 0;
4631
+ const done = calls.filter((call) => call.toolMessage).length;
4632
+ const statuses = calls.map((call) => entryStatus(call, isActive));
4633
+ const failedIds = new Set(calls.filter((_, index) => statuses[index] === "failed").map((call) => call.toolCall.id));
4634
+ const status = current ? "running" : failedIds.size > 0 ? "failed" : statuses.includes("stopped") ? "stopped" : "done";
4635
+ const rows = (rowAppearance) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCallGroupContext.Provider, {
4636
+ value: {
4637
+ appearance: rowAppearance,
4638
+ failedIds,
4639
+ active: isActive
4640
+ },
4641
+ children: calls.map((call) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.default.Fragment, { children: renderCall(call) }, call.toolCall.id))
4642
+ });
4643
+ const groupProps = {
4644
+ "data-copilotkit": true,
4645
+ "data-testid": "copilot-tool-call-group",
4646
+ "data-appearance": appearance,
4647
+ "data-state": status,
4648
+ ...props
4649
+ };
4650
+ const runningLabel = current && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4651
+ className: "cpk:shrink-0 cpk:font-medium cpk-shimmer",
4652
+ children: ["Running ", current.toolCall.function.name]
4653
+ }), preview && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4654
+ className: "cpk:min-w-0 cpk:truncate cpk:font-mono cpk:text-xs cpk:opacity-70",
4655
+ children: preview
4656
+ })] });
4657
+ if (appearance === "line") {
4658
+ if (calls.length === 1 && !current) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4659
+ ...groupProps,
4660
+ className: (0, tailwind_merge.twMerge)("cpk:my-2", className),
4661
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCallGroupContext.Provider, {
4662
+ value: {
4663
+ appearance: "line",
4664
+ failedIds,
4665
+ active: isActive
4666
+ },
4667
+ children: renderCall(latest)
4668
+ })
4669
+ });
4670
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4671
+ ...groupProps,
4672
+ className: (0, tailwind_merge.twMerge)("cpk:my-3", className),
4673
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4674
+ type: "button",
4675
+ "aria-expanded": open,
4676
+ "aria-controls": bodyId,
4677
+ onClick: () => setOpen((value) => !value),
4678
+ className: "cpk:m-0 cpk:inline-flex cpk:max-w-full cpk:min-w-0 cpk:cursor-pointer cpk:items-center cpk:gap-1.5 cpk:rounded-md cpk:border-none cpk:bg-transparent cpk:p-0 cpk:py-0.5 cpk:text-sm cpk:text-muted-foreground cpk:transition-colors cpk:hover:text-foreground cpk:focus-visible:outline-2 cpk:focus-visible:outline-ring",
4679
+ children: [runningLabel || /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4680
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4681
+ className: "cpk:shrink-0 cpk:font-medium",
4682
+ children: ["Used ", pluralize(calls.length, "tool")]
4683
+ }),
4684
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4685
+ className: "cpk:min-w-0 cpk:truncate cpk:font-mono cpk:text-xs cpk:opacity-70",
4686
+ children: toolNames(calls)
4687
+ }),
4688
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FailedCount, { calls })
4689
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron, { open })]
4690
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Collapsible, {
4691
+ open,
4692
+ id: bodyId,
4693
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4694
+ className: "cpk:mt-1 cpk:ml-1 cpk:border-l-2 cpk:border-border cpk:py-0.5 cpk:pl-3",
4695
+ children: rows("line")
4696
+ })
4697
+ })]
4698
+ });
4699
+ }
4700
+ if (appearance === "stack") {
4701
+ if (calls.length === 1) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4702
+ ...groupProps,
4703
+ className: (0, tailwind_merge.twMerge)("cpk:py-1", className),
4704
+ children: renderCall(latest)
4705
+ });
4706
+ const behind = open ? 0 : Math.min(calls.length - 1, 2);
4707
+ const layer = "cpk:rounded-xl cpk:border cpk:border-border cpk:bg-card cpk:text-card-foreground";
4708
+ const latestStatus = entryStatus(latest, isActive);
4709
+ const latestPreview = entryPreview(latest);
4710
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4711
+ ...groupProps,
4712
+ className: (0, tailwind_merge.twMerge)("cpk:my-3", className),
4713
+ style: {
4714
+ paddingBottom: behind * 6,
4715
+ ...props.style
4716
+ },
4717
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4718
+ className: "cpk:relative cpk:isolate",
4719
+ children: [Array.from({ length: behind }, (_, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4720
+ "aria-hidden": "true",
4721
+ className: (0, tailwind_merge.twMerge)(layer, "cpk:absolute cpk:inset-0"),
4722
+ style: {
4723
+ transform: `translateY(${(index + 1) * 6}px) scaleX(${1 - (index + 1) * .045})`,
4724
+ zIndex: -1 - index,
4725
+ opacity: 1 - (index + 1) * .28
4726
+ }
4727
+ }, index)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4728
+ type: "button",
4729
+ "aria-expanded": open,
4730
+ "aria-controls": open ? bodyId : void 0,
4731
+ onClick: () => setOpen((value) => !value),
4732
+ className: (0, tailwind_merge.twMerge)("cpk:relative cpk:m-0 cpk:flex cpk:min-w-0 cpk:cursor-pointer cpk:items-center cpk:text-left cpk:transition-colors cpk:focus-visible:outline-2 cpk:focus-visible:outline-ring", open ? "cpk:mb-1 cpk:gap-1.5 cpk:rounded-md cpk:border-none cpk:bg-transparent cpk:p-0 cpk:py-0.5 cpk:text-xs cpk:text-muted-foreground cpk:hover:text-foreground" : (0, tailwind_merge.twMerge)(layer, "cpk:w-full cpk:gap-2 cpk:px-3.5 cpk:py-2.5 cpk:text-[13px] cpk:shadow-[0_1px_2px_rgb(0_0_0/0.04)] cpk:hover:bg-accent")),
4733
+ children: open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4734
+ pluralize(calls.length, "tool call"),
4735
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FailedCount, { calls }),
4736
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron, {
4737
+ open: true,
4738
+ className: "cpk:-rotate-90"
4739
+ }),
4740
+ "Collapse"
4741
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4742
+ className: "cpk:flex cpk:min-w-0 cpk:flex-1 cpk:items-center cpk:gap-2 cpk:animate-[cpk-stack-drop_260ms_cubic-bezier(0.16,1,0.3,1)] cpk:motion-reduce:animate-none",
4743
+ children: [
4744
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusIcon, { status: latestStatus }),
4745
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4746
+ className: (0, tailwind_merge.twMerge)("cpk:shrink-0 cpk:font-mono cpk:font-medium cpk:text-foreground", latestStatus === "running" && "cpk-shimmer"),
4747
+ children: latest.toolCall.function.name
4748
+ }),
4749
+ latestPreview && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4750
+ className: "cpk:min-w-0 cpk:truncate cpk:font-mono cpk:text-muted-foreground",
4751
+ children: latestPreview
4752
+ })
4753
+ ]
4754
+ }, latest.toolCall.id), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4755
+ className: "cpk:ml-2 cpk:inline-flex cpk:shrink-0 cpk:items-center cpk:gap-2 cpk:text-xs cpk:text-muted-foreground",
4756
+ children: [
4757
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FailedCount, { calls }),
4758
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4759
+ className: "cpk:tabular-nums",
4760
+ children: pluralize(calls.length, "call")
4761
+ }),
4762
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron, {
4763
+ open: false,
4764
+ className: "cpk:rotate-90"
4765
+ })
4766
+ ]
4767
+ })] })
4768
+ })]
4769
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4770
+ id: bodyId,
4771
+ className: "cpk:[&>*]:animate-[cpk-stack-fan_300ms_cubic-bezier(0.16,1,0.3,1)_both] cpk:motion-reduce:[&>*]:animate-none",
4772
+ children: calls.map((call) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.default.Fragment, { children: renderCall(call) }, call.toolCall.id))
4773
+ })]
4774
+ });
4775
+ }
4776
+ if (calls.length === 1) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4777
+ ...groupProps,
4778
+ className: (0, tailwind_merge.twMerge)("cpk:py-1", className),
4779
+ children: renderCall(latest)
4780
+ });
4781
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4782
+ ...groupProps,
4783
+ className: (0, tailwind_merge.twMerge)("cpk:my-3 cpk:overflow-hidden cpk:rounded-xl cpk:border cpk:border-border cpk:bg-card cpk:text-card-foreground", className),
4784
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4785
+ type: "button",
4786
+ "aria-expanded": open,
4787
+ "aria-controls": bodyId,
4788
+ onClick: () => setOpen((value) => !value),
4789
+ className: "cpk:m-0 cpk:flex cpk:w-full cpk:min-w-0 cpk:cursor-pointer cpk:items-center cpk:gap-2.5 cpk:border-none cpk:bg-transparent cpk:px-3.5 cpk:py-2.5 cpk:text-left cpk:text-[13px] cpk:text-inherit cpk:transition-colors cpk:hover:bg-accent/60 cpk:focus-visible:outline-2 cpk:focus-visible:-outline-offset-2 cpk:focus-visible:outline-ring",
4790
+ children: [
4791
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusIcon, { status }),
4792
+ runningLabel || /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4793
+ className: "cpk:shrink-0 cpk:font-medium cpk:text-foreground",
4794
+ children: ["Used ", pluralize(calls.length, "tool")]
4795
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4796
+ className: "cpk:min-w-0 cpk:truncate cpk:font-mono cpk:text-xs cpk:text-muted-foreground",
4797
+ children: toolNames(calls)
4798
+ })] }),
4799
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4800
+ className: "cpk:ml-auto cpk:flex cpk:shrink-0 cpk:items-center cpk:gap-2 cpk:pl-2 cpk:text-xs cpk:text-muted-foreground",
4801
+ children: [status === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FailedCount, { calls }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4802
+ className: "cpk:tabular-nums",
4803
+ children: status === "running" ? `${done} done` : statusLabels[status]
4804
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron, { open })]
4805
+ })
4806
+ ]
4807
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Collapsible, {
4808
+ open,
4809
+ id: bodyId,
4810
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: rows("card") })
4811
+ })]
4812
+ });
4813
+ }
4814
+
4304
4815
  //#endregion
4305
4816
  //#region src/v2/hooks/use-default-render-tool.tsx
4306
4817
  /**
@@ -4384,7 +4895,8 @@ function useDefaultRenderTool(config, deps) {
4384
4895
  const userRender = config?.render;
4385
4896
  useRenderTool({
4386
4897
  name: "*",
4387
- render: userRender ? (raw) => userRender(adaptRendererProps(raw)) : (raw) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultToolCallRenderer, { ...adaptRendererProps(raw) })
4898
+ render: userRender ? (raw) => userRender(adaptRendererProps(raw)) : (raw) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultToolCallRenderer, { ...adaptRendererProps(raw) }),
4899
+ group: userRender ? config?.group === true : true
4388
4900
  }, deps);
4389
4901
  }
4390
4902
  /**
@@ -4405,9 +4917,23 @@ function safeStringifyForPre(value) {
4405
4917
  }
4406
4918
  }
4407
4919
  function DefaultToolCallRenderer({ name, toolCallId, parameters, status, result }) {
4920
+ const group = (0, react.useContext)(ToolCallGroupContext);
4408
4921
  const [isExpanded, setIsExpanded] = (0, react.useState)(false);
4409
4922
  const isActive = status === "inProgress" || status === "executing";
4410
4923
  const isComplete = status === "complete";
4924
+ if (group) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCallRow, {
4925
+ "data-testid": "copilot-tool-render",
4926
+ "data-tool-name": name,
4927
+ "data-tool-call-id": toolCallId,
4928
+ "data-status": status,
4929
+ "data-args": safeStringifyForAttr(parameters),
4930
+ "data-result": safeStringifyForAttr(result),
4931
+ name,
4932
+ parameters,
4933
+ result,
4934
+ status: isComplete ? group.failedIds.has(toolCallId) ? "failed" : "done" : group.active ? "running" : "stopped",
4935
+ appearance: group.appearance
4936
+ });
4411
4937
  const statusLabel = isActive ? "Running" : isComplete ? "Done" : status;
4412
4938
  const dotClassName = isActive ? "cpk:bg-amber-500" : isComplete ? "cpk:bg-emerald-500" : "cpk:bg-zinc-400";
4413
4939
  const badgeClassName = isActive ? "cpk:bg-amber-100 cpk:text-amber-800 cpk:dark:bg-amber-500/15 cpk:dark:text-amber-400" : isComplete ? "cpk:bg-emerald-100 cpk:text-emerald-800 cpk:dark:bg-emerald-500/15 cpk:dark:text-emerald-400" : "cpk:bg-zinc-100 cpk:text-zinc-800 cpk:dark:bg-zinc-700/40 cpk:dark:text-zinc-300";
@@ -6104,38 +6630,227 @@ function useLearningContainersInCurrentThread({ learningContainers }) {
6104
6630
  });
6105
6631
  }
6106
6632
 
6633
+ //#endregion
6634
+ //#region src/v2/components/chat/tool-call-display-context.ts
6635
+ /** @internal */
6636
+ const ToolCallDisplayContext = (0, react.createContext)(null);
6637
+
6638
+ //#endregion
6639
+ //#region src/v2/components/chat/tool-call-layout.ts
6640
+ function contentLength(content) {
6641
+ return typeof content === "string" ? content.length : JSON.stringify(content ?? "").length;
6642
+ }
6643
+ const entrySignature = ({ toolCall, toolMessage }) => [
6644
+ toolCall.id,
6645
+ toolCall.function.name,
6646
+ toolCall.function.arguments.length,
6647
+ toolMessage ? `${contentLength(toolMessage.content)}${toolMessage.error ? "!" : ""}` : "-"
6648
+ ].join(":");
6649
+ function signatureOf(segments) {
6650
+ return segments.map((segment) => segment.kind === "call" ? `c(${entrySignature(segment.call)})` : `g${segment.trailing ? "*" : ""}(${segment.calls.map(entrySignature).join(",")})`).join("|");
6651
+ }
6652
+ /** Index of tool results by the call they answer. */
6653
+ function getToolResults(messages) {
6654
+ const results = /* @__PURE__ */ new Map();
6655
+ for (const message of messages) if (message.role === "tool") results.set(message.toolCallId, message);
6656
+ return results;
6657
+ }
6658
+ /**
6659
+ * Lays out the tool calls of every assistant message in `rendered`.
6660
+ *
6661
+ * Calls drawn by a renderer that opted into grouping join the group that's
6662
+ * open; anything else that shows between them closes it: assistant text, a
6663
+ * user, reasoning or activity message, or a call drawn by any other renderer. A group belongs to
6664
+ * the message where it starts, so later messages whose calls joined it render
6665
+ * no tool calls themselves.
6666
+ *
6667
+ * @param rendered - The messages as the chat renders them, in order.
6668
+ * @param all - Every message, for finding tool results (a transform may hide them).
6669
+ * @param renderers - The registered renderers.
6670
+ */
6671
+ function getToolCallLayouts(rendered, all, renderers) {
6672
+ const results = getToolResults(all);
6673
+ const layouts = /* @__PURE__ */ new Map();
6674
+ let open;
6675
+ for (const message of rendered) {
6676
+ if (message.role === "tool") continue;
6677
+ if (message.role !== "assistant") {
6678
+ open = void 0;
6679
+ continue;
6680
+ }
6681
+ if (typeof message.content === "string" && message.content.trim()) open = void 0;
6682
+ const segments = [];
6683
+ layouts.set(message.id, segments);
6684
+ const toolCalls = /* @__PURE__ */ new Map();
6685
+ for (const toolCall of message.toolCalls ?? []) toolCalls.set(toolCall.id, toolCall);
6686
+ for (const toolCall of toolCalls.values()) {
6687
+ const entry = {
6688
+ toolCall,
6689
+ toolMessage: results.get(toolCall.id)
6690
+ };
6691
+ const name = toolCall.function.name;
6692
+ const named = renderers.named.has(name);
6693
+ const rendered = named || renderers.wildcard;
6694
+ const groups = named ? renderers.groupedNames.has(name) : renderers.wildcardGroups;
6695
+ if (!rendered || !groups) {
6696
+ if (rendered) open = void 0;
6697
+ segments.push({
6698
+ kind: "call",
6699
+ id: toolCall.id,
6700
+ call: entry,
6701
+ rendered
6702
+ });
6703
+ } else if (open) open.calls.push(entry);
6704
+ else {
6705
+ open = {
6706
+ kind: "group",
6707
+ id: toolCall.id,
6708
+ calls: [entry],
6709
+ trailing: false
6710
+ };
6711
+ segments.push(open);
6712
+ }
6713
+ }
6714
+ }
6715
+ if (open) open.trailing = true;
6716
+ const layout = /* @__PURE__ */ new Map();
6717
+ for (const [id, segments] of layouts) layout.set(id, {
6718
+ segments,
6719
+ signature: signatureOf(segments)
6720
+ });
6721
+ return layout;
6722
+ }
6723
+ /**
6724
+ * Finds the working phase of every reply in `rendered`: its messages up to the
6725
+ * last one that calls a tool. Text in there was narration; what follows is the
6726
+ * reply itself. Replies that called no tools are omitted, except the latest,
6727
+ * which may be about to.
6728
+ */
6729
+ function getTurnWork(rendered, layouts) {
6730
+ const turns = /* @__PURE__ */ new Map();
6731
+ let start = 0;
6732
+ while (start < rendered.length) {
6733
+ let end = start;
6734
+ while (end < rendered.length && rendered[end].role !== "user") end++;
6735
+ const key = start > 0 ? rendered[start - 1].id : "start";
6736
+ const visible = rendered.slice(start, end).filter((message) => message.role !== "tool");
6737
+ const isLatest = end === rendered.length;
6738
+ const calls = [];
6739
+ for (const message of visible) for (const segment of layouts.get(message.id)?.segments ?? []) if (segment.kind === "group") calls.push(...segment.calls);
6740
+ else if (segment.rendered) calls.push(segment.call);
6741
+ const shown = new Set(calls.map((entry) => entry.toolCall.id));
6742
+ let lastWork = -1;
6743
+ visible.forEach((message, index) => {
6744
+ if (message.role === "assistant" && message.toolCalls?.some((call) => shown.has(call.id))) lastWork = index;
6745
+ });
6746
+ const first = visible[0];
6747
+ if (first && (calls.length > 0 || isLatest)) {
6748
+ let lastAssistant = -1;
6749
+ visible.forEach((message, index) => {
6750
+ if (message.role === "assistant") lastAssistant = index;
6751
+ });
6752
+ const replyText = visible[lastAssistant]?.content;
6753
+ const endsWithWork = lastAssistant >= 0 && lastAssistant <= lastWork && typeof replyText === "string" && replyText.trim().length > 0;
6754
+ turns.set(key, {
6755
+ key,
6756
+ firstMessageId: first.id,
6757
+ workMessageIds: new Set(visible.slice(0, lastWork + 1).map((message) => message.id)),
6758
+ replyTextMessageId: endsWithWork ? visible[lastAssistant].id : void 0,
6759
+ calls,
6760
+ isLatest
6761
+ });
6762
+ }
6763
+ start = end + 1;
6764
+ }
6765
+ return turns;
6766
+ }
6767
+
6107
6768
  //#endregion
6108
6769
  //#region src/v2/components/chat/CopilotChatToolCallsView.tsx
6109
- function CopilotChatToolCallsView({ message, messages = [] }) {
6770
+ const appearances = {
6771
+ grouped: "card",
6772
+ line: "line",
6773
+ stacked: "stack"
6774
+ };
6775
+ function CopilotChatToolCallsView({ message, messages = [], display: displayProp, group }) {
6110
6776
  const renderToolCall = useRenderToolCall();
6777
+ const rendererNames = useToolCallRendererNames();
6111
6778
  const { copilotkit } = (0, _copilotkit_react_core_v2_context.useCopilotKit)();
6112
6779
  const subscribe = (0, react.useCallback)((onChange) => copilotkit.ɵsubscribeToLearningConfigured(onChange), [copilotkit]);
6113
6780
  const getSnapshot = () => copilotkit.ɵlearningConfigured;
6114
6781
  const learningConfigured = (0, react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
6782
+ const context = (0, react.useContext)(ToolCallDisplayContext);
6783
+ const display = displayProp ?? context?.display ?? "grouped";
6784
+ const viewLayout = context?.layouts.get(message.id);
6785
+ const layout = (0, react.useMemo)(() => {
6786
+ if (display === "separate") return void 0;
6787
+ return viewLayout ?? getToolCallLayouts([message], messages, rendererNames).get(message.id);
6788
+ }, [
6789
+ display,
6790
+ viewLayout,
6791
+ message,
6792
+ messages,
6793
+ rendererNames
6794
+ ]);
6115
6795
  if (!message.toolCalls || message.toolCalls.length === 0) return null;
6116
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: message.toolCalls.map((toolCall) => {
6796
+ const renderCall = (entry) => {
6117
6797
  const rendered = renderToolCall({
6118
- toolCall,
6119
- toolMessage: messages.find((m) => m.role === "tool" && m.toolCallId === toolCall.id)
6798
+ toolCall: entry.toolCall,
6799
+ toolMessage: entry.toolMessage
6120
6800
  });
6121
- if (rendered === null) return null;
6122
- if (!learningConfigured) return rendered;
6801
+ if (rendered === null || !learningConfigured) return rendered;
6123
6802
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6124
- "data-tool-call-id": toolCall.id,
6803
+ "data-tool-call-id": entry.toolCall.id,
6125
6804
  style: { display: "contents" },
6126
6805
  children: rendered
6127
- }, toolCall.id);
6806
+ });
6807
+ };
6808
+ if (!layout) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: message.toolCalls.map((toolCall) => {
6809
+ const toolMessage = messages.find((m) => m.role === "tool" && m.toolCallId === toolCall.id);
6810
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.default.Fragment, { children: renderCall({
6811
+ toolCall,
6812
+ toolMessage
6813
+ }) }, toolCall.id);
6814
+ }) });
6815
+ const workOpen = context?.workOpen.get(message.id);
6816
+ const groupActive = (trailing) => trailing && (context ? context.isRunning : true);
6817
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: layout.segments.map((segment) => {
6818
+ if (segment.kind === "call") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.default.Fragment, { children: renderCall(segment.call) }, segment.id);
6819
+ if (display === "turn") {
6820
+ const failedIds = new Set(segment.calls.filter((call) => call.toolMessage?.error).map((call) => call.toolCall.id));
6821
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Collapsible, {
6822
+ open: workOpen ?? true,
6823
+ className: "cpk:-mx-2",
6824
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6825
+ className: "cpk:px-2 cpk:py-1.5",
6826
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCallGroupContext.Provider, {
6827
+ value: {
6828
+ appearance: "line",
6829
+ failedIds,
6830
+ active: groupActive(segment.trailing)
6831
+ },
6832
+ children: segment.calls.map((call) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.default.Fragment, { children: renderCall(call) }, call.toolCall.id))
6833
+ })
6834
+ })
6835
+ }, segment.id);
6836
+ }
6837
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.default.Fragment, { children: renderSlot(group, CopilotChatToolCallGroup, {
6838
+ calls: segment.calls,
6839
+ renderCall,
6840
+ appearance: appearances[display] ?? "card",
6841
+ isActive: groupActive(segment.trailing)
6842
+ }) }, segment.id);
6128
6843
  }) });
6129
6844
  }
6130
6845
 
6131
6846
  //#endregion
6132
6847
  //#region src/v2/components/chat/CopilotChatInspectorButton.tsx
6133
6848
  let shortcutsHidden = false;
6134
- const listeners = /* @__PURE__ */ new Set();
6849
+ const listeners$1 = /* @__PURE__ */ new Set();
6135
6850
  const subscribe = (listener) => {
6136
- listeners.add(listener);
6851
+ listeners$1.add(listener);
6137
6852
  return () => {
6138
- listeners.delete(listener);
6853
+ listeners$1.delete(listener);
6139
6854
  };
6140
6855
  };
6141
6856
  const getSnapshot = () => shortcutsHidden;
@@ -6145,7 +6860,7 @@ function useInspectorShortcutsHidden() {
6145
6860
  }
6146
6861
  function hideShortcuts() {
6147
6862
  shortcutsHidden = true;
6148
- listeners.forEach((listener) => listener());
6863
+ listeners$1.forEach((listener) => listener());
6149
6864
  }
6150
6865
  function InspectorKiteIcon({ className, ...props }) {
6151
6866
  const id = (0, react.useId)();
@@ -6411,9 +7126,321 @@ function CopilotChatInspectorButton({ title, className, onClick, onPointerDown,
6411
7126
  });
6412
7127
  }
6413
7128
 
7129
+ //#endregion
7130
+ //#region src/v2/components/ui/popover.tsx
7131
+ function Popover({ ...props }) {
7132
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_react_popover.Root, {
7133
+ "data-slot": "popover",
7134
+ ...props
7135
+ });
7136
+ }
7137
+ function PopoverAnchor({ ...props }) {
7138
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_react_popover.Anchor, {
7139
+ "data-slot": "popover-anchor",
7140
+ ...props
7141
+ });
7142
+ }
7143
+ /**
7144
+ * Popover content in a portal, so it floats over the page instead of taking
7145
+ * room in the layout, with the CopilotKit style scope on it.
7146
+ */
7147
+ const PopoverContent = react.forwardRef(({ className, align = "start", sideOffset = 6, ...props }, ref) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_react_popover.Portal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_react_popover.Content, {
7148
+ ref,
7149
+ "data-copilotkit": true,
7150
+ "data-copilotkit-popover": true,
7151
+ "data-slot": "popover-content",
7152
+ align,
7153
+ sideOffset,
7154
+ className: cn("cpk:z-50 cpk:outline-none cpk:data-[state=open]:animate-in cpk:data-[state=open]:fade-in-0 cpk:data-[state=open]:zoom-in-95 cpk:data-[side=bottom]:slide-in-from-top-1 cpk:data-[side=top]:slide-in-from-bottom-1", className),
7155
+ ...props
7156
+ }) }));
7157
+ PopoverContent.displayName = "PopoverContent";
7158
+
7159
+ //#endregion
7160
+ //#region src/v2/lib/message-feedback.ts
7161
+ /** Trajectory event name for a rating of one assistant message. */
7162
+ const MESSAGE_FEEDBACK_EVENT_NAME = "message.feedback";
7163
+ /** Longest comment kept, in characters; the rest is cut so the event stays small. */
7164
+ const MESSAGE_FEEDBACK_MAX_COMMENT_LENGTH = 1e3;
7165
+ /** Longest excerpt of the rated response sent with the rating, in characters. */
7166
+ const RESPONSE_EXCERPT_LENGTH = 500;
7167
+ const ratings = /* @__PURE__ */ new Map();
7168
+ const listeners = /* @__PURE__ */ new Map();
7169
+ const threadKey = (agentId, threadId) => JSON.stringify([agentId, threadId]);
7170
+ const messageKey = (agentId, threadId, messageId) => JSON.stringify([
7171
+ agentId,
7172
+ threadId,
7173
+ messageId
7174
+ ]);
7175
+ /** Cuts `text` to `max` characters without splitting a surrogate pair. */
7176
+ const clip = (text, max) => {
7177
+ const characters = Array.from(text);
7178
+ return characters.length > max ? characters.slice(0, max).join("") : text;
7179
+ };
7180
+ /** The comment as kept and sent: trimmed and capped, or none. */
7181
+ const normalizeComment = (comment) => {
7182
+ if (typeof comment !== "string") return void 0;
7183
+ const text = clip(comment.trim(), MESSAGE_FEEDBACK_MAX_COMMENT_LENGTH);
7184
+ return text === "" ? void 0 : text;
7185
+ };
7186
+ /** The rating this page gave one message, if any. */
7187
+ function getMessageFeedback(agentId, threadId, messageId) {
7188
+ return ratings.get(messageKey(agentId, threadId, messageId));
7189
+ }
7190
+ /** Calls `listener` whenever a rating in this Thread changes. */
7191
+ function subscribeToMessageFeedback(agentId, threadId, listener) {
7192
+ const key = threadKey(agentId, threadId);
7193
+ let set = listeners.get(key);
7194
+ if (set === void 0) {
7195
+ set = /* @__PURE__ */ new Set();
7196
+ listeners.set(key, set);
7197
+ }
7198
+ set.add(listener);
7199
+ return () => {
7200
+ set.delete(listener);
7201
+ if (set.size === 0) listeners.delete(key);
7202
+ };
7203
+ }
7204
+ /**
7205
+ * Sets (or, with `null`, clears) this page's rating of one message and sends
7206
+ * it to the active Trajectory. Every change is a new event; the newest event
7207
+ * for a `feedbackId` is the current one. Like any Trajectory event, a change
7208
+ * made while no Trajectory captures, for example during a reconnect, is not
7209
+ * sent; the page still shows it.
7210
+ */
7211
+ function setMessageFeedback(params) {
7212
+ const { copilotkit, agentId, threadId, message } = params;
7213
+ const comment = normalizeComment(params.feedback?.comment);
7214
+ const feedback = params.feedback && {
7215
+ rating: params.feedback.rating,
7216
+ ...comment ? { comment } : {}
7217
+ };
7218
+ const key = messageKey(agentId, threadId, message.id);
7219
+ if (feedback) ratings.set(key, feedback);
7220
+ else ratings.delete(key);
7221
+ for (const listener of listeners.get(threadKey(agentId, threadId)) ?? []) listener();
7222
+ const runId = copilotkit.getRunIdForMessage(agentId, threadId, message.id);
7223
+ copilotkit.emitTrajectoryEvent(MESSAGE_FEEDBACK_EVENT_NAME, {
7224
+ feedbackId: `${threadId}:${message.id}`,
7225
+ threadId,
7226
+ messageId: message.id,
7227
+ agentId,
7228
+ ...runId ? { runId } : {},
7229
+ rating: feedback?.rating ?? null,
7230
+ ...feedback?.comment ? { comment: feedback.comment } : {},
7231
+ ...copilotkit.ɵlearningCapturesAgentText ? { response: clip(message.content ?? "", RESPONSE_EXCERPT_LENGTH) } : {}
7232
+ });
7233
+ }
7234
+
7235
+ //#endregion
7236
+ //#region src/v2/components/chat/message-feedback.tsx
7237
+ /**
7238
+ * Set by the message view while Learning is on for this chat. Assistant
7239
+ * messages without it show thumbs only when the app passes its own handlers
7240
+ * or thumb slots.
7241
+ */
7242
+ const MessageFeedbackScopeContext = (0, react.createContext)(null);
7243
+ const capturedCores = /* @__PURE__ */ new WeakSet();
7244
+ const rememberCapture = (copilotkit) => {
7245
+ if (capturedCores.has(copilotkit)) return;
7246
+ capturedCores.add(copilotkit);
7247
+ const unsubscribe = copilotkit.ɵsubscribeToLearningConfigured(() => {
7248
+ if (copilotkit.ɵlearningConfigured) return;
7249
+ capturedCores.delete(copilotkit);
7250
+ unsubscribe();
7251
+ });
7252
+ };
7253
+ /**
7254
+ * Whether the thumbs record feedback for Learning. True once a Trajectory of
7255
+ * this Core has started and as long as Learning stays configured, also while
7256
+ * capture reconnects or is stopped, so the buttons do not flicker. Ratings
7257
+ * given meanwhile still show but, like any Trajectory event, are not
7258
+ * sent. A capture that never starts (for example without a license) leaves it
7259
+ * false.
7260
+ */
7261
+ function useLearningFeedbackEnabled(copilotkit) {
7262
+ const configured = (0, react.useSyncExternalStore)((0, react.useCallback)((onChange) => copilotkit.ɵsubscribeToLearningConfigured(onChange), [copilotkit]), () => copilotkit.ɵlearningConfigured, () => false);
7263
+ const capturing = (0, react.useSyncExternalStore)((0, react.useCallback)((onChange) => copilotkit.subscribe({ onTrajectoryChanged: onChange }).unsubscribe, [copilotkit]), () => copilotkit.trajectoryId !== null, () => false);
7264
+ (0, react.useEffect)(() => {
7265
+ if (capturing) rememberCapture(copilotkit);
7266
+ }, [capturing, copilotkit]);
7267
+ return configured && (capturing || capturedCores.has(copilotkit));
7268
+ }
7269
+ const noSubscription = () => () => {};
7270
+ /** Feedback controls for `message`, or `null` when this chat collects none. */
7271
+ function useMessageFeedback(message) {
7272
+ const scope = (0, react.useContext)(MessageFeedbackScopeContext);
7273
+ const subscribe = (0, react.useCallback)((onChange) => scope ? subscribeToMessageFeedback(scope.agentId, scope.threadId, onChange) : noSubscription(), [scope]);
7274
+ const getSnapshot = () => scope ? getMessageFeedback(scope.agentId, scope.threadId, message.id) : void 0;
7275
+ const feedback = (0, react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
7276
+ if (!scope) return null;
7277
+ const save = (next) => setMessageFeedback({
7278
+ ...scope,
7279
+ message,
7280
+ feedback: next
7281
+ });
7282
+ return {
7283
+ feedback,
7284
+ rate: (rating) => {
7285
+ if (feedback?.rating !== rating) save({ rating });
7286
+ },
7287
+ clear: () => {
7288
+ if (feedback) save(null);
7289
+ },
7290
+ comment: (comment) => {
7291
+ if (feedback) save({
7292
+ rating: feedback.rating,
7293
+ comment
7294
+ });
7295
+ }
7296
+ };
7297
+ }
7298
+ /**
7299
+ * How long the pointer may be away from the rating and its comment before the
7300
+ * comment closes: enough to cross the gap between them, short enough that
7301
+ * passing over a rating does not leave it open.
7302
+ */
7303
+ const FEEDBACK_COMMENT_CLOSE_DELAY_MS = 150;
7304
+ /**
7305
+ * Drives the comment popover of a rated message.
7306
+ *
7307
+ * It opens when a rating is given and whenever the pointer or keyboard focus
7308
+ * reaches the chosen thumb, with the saved comment in the field. It closes by
7309
+ * itself shortly after the pointer leaves the thumb and the popover, unless
7310
+ * focus is in the field: then it stays until focus moves away, Enter, or
7311
+ * Escape. Closing saves a changed comment; Escape, a click on a thumb, and
7312
+ * removing the rating discard it.
7313
+ */
7314
+ function useFeedbackComment(controls) {
7315
+ const [open, setOpen] = (0, react.useState)(false);
7316
+ const [draft, setDraft] = (0, react.useState)("");
7317
+ const [focusRequest, setFocusRequest] = (0, react.useState)(0);
7318
+ const inputRef = (0, react.useRef)(null);
7319
+ const timer = (0, react.useRef)(void 0);
7320
+ const editing = (0, react.useRef)(false);
7321
+ const focusWaiting = (0, react.useRef)(false);
7322
+ const pointerOver = (0, react.useRef)(false);
7323
+ const returning = (0, react.useRef)(false);
7324
+ const latest = (0, react.useRef)({
7325
+ controls,
7326
+ draft,
7327
+ open
7328
+ });
7329
+ latest.current = {
7330
+ ...latest.current,
7331
+ controls,
7332
+ draft,
7333
+ open
7334
+ };
7335
+ const takeFocusRequest = (0, react.useCallback)(() => {
7336
+ const waiting = focusWaiting.current;
7337
+ focusWaiting.current = false;
7338
+ return waiting;
7339
+ }, []);
7340
+ const cancelClose = (0, react.useCallback)(() => {
7341
+ if (timer.current === void 0) return;
7342
+ clearTimeout(timer.current);
7343
+ timer.current = void 0;
7344
+ }, []);
7345
+ (0, react.useEffect)(() => cancelClose, [cancelClose]);
7346
+ const savedComment = (0, react.useCallback)(() => latest.current.controls?.feedback?.comment ?? "", []);
7347
+ (0, react.useEffect)(() => () => {
7348
+ const { open: wasOpen, controls: current, draft: text } = latest.current;
7349
+ if (wasOpen && current?.feedback && text.trim() !== savedComment()) current.comment(text);
7350
+ }, []);
7351
+ const close = (0, react.useCallback)(({ save = true } = {}) => {
7352
+ cancelClose();
7353
+ focusWaiting.current = false;
7354
+ pointerOver.current = false;
7355
+ if (!latest.current.open) return;
7356
+ const current = latest.current.controls;
7357
+ if (save && current?.feedback && latest.current.draft.trim() !== savedComment()) current.comment(latest.current.draft);
7358
+ editing.current = false;
7359
+ latest.current = {
7360
+ ...latest.current,
7361
+ open: false
7362
+ };
7363
+ setOpen(false);
7364
+ }, [cancelClose, savedComment]);
7365
+ const show = ({ comment, focus }) => {
7366
+ cancelClose();
7367
+ latest.current = {
7368
+ ...latest.current,
7369
+ open: true,
7370
+ draft: comment
7371
+ };
7372
+ setDraft(comment);
7373
+ if (focus) {
7374
+ focusWaiting.current = true;
7375
+ setFocusRequest((request) => request + 1);
7376
+ }
7377
+ setOpen(true);
7378
+ };
7379
+ return {
7380
+ open,
7381
+ draft,
7382
+ setDraft,
7383
+ focusRequest,
7384
+ takeFocusRequest,
7385
+ inputRef,
7386
+ show,
7387
+ enter: (via = "focus") => {
7388
+ cancelClose();
7389
+ if (via === "pointer") pointerOver.current = true;
7390
+ if (returning.current) return;
7391
+ if (!latest.current.open && latest.current.controls?.feedback) show({
7392
+ comment: savedComment(),
7393
+ focus: false
7394
+ });
7395
+ },
7396
+ leave: (via = "focus") => {
7397
+ if (via === "pointer") pointerOver.current = false;
7398
+ else if (pointerOver.current) return;
7399
+ cancelClose();
7400
+ timer.current = setTimeout(() => {
7401
+ timer.current = void 0;
7402
+ if (!editing.current) close();
7403
+ }, FEEDBACK_COMMENT_CLOSE_DELAY_MS);
7404
+ },
7405
+ setEditing: (value) => {
7406
+ editing.current = value;
7407
+ },
7408
+ close,
7409
+ returnFocus: (target) => {
7410
+ returning.current = true;
7411
+ try {
7412
+ target?.focus();
7413
+ } finally {
7414
+ returning.current = false;
7415
+ }
7416
+ }
7417
+ };
7418
+ }
7419
+
6414
7420
  //#endregion
6415
7421
  //#region src/v2/components/chat/CopilotChatAssistantMessage.tsx
6416
7422
  /**
7423
+ * In the `"turn"` tool-call display, text in a reply's working phase was
7424
+ * narration: it fades to muted once a call follows it, and folds away with the
7425
+ * rest of the work when the reply is done. The wrapper is there for every
7426
+ * message in that display, so text that turns into narration keeps its node
7427
+ * (and its place) rather than remounting.
7428
+ */
7429
+ function WorkText({ messageId, children }) {
7430
+ const context = (0, react.useContext)(ToolCallDisplayContext);
7431
+ if (context?.display !== "turn") return children;
7432
+ const open = context.workTextOpen.get(messageId);
7433
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Collapsible, {
7434
+ open: open ?? true,
7435
+ className: "cpk:-mx-2",
7436
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7437
+ className: "cpk:px-2",
7438
+ "data-work-text": open === void 0 ? "reply" : "narration",
7439
+ children
7440
+ })
7441
+ });
7442
+ }
7443
+ /**
6417
7444
  * Binds the Inspector action to the active chat without making the full
6418
7445
  * assistant message subscribe to chat configuration changes.
6419
7446
  */
@@ -6428,31 +7455,216 @@ function BoundInspectorButton({ inspectorButton, messageId, openInspector, ...pr
6428
7455
  })
6429
7456
  });
6430
7457
  }
6431
- function CopilotChatAssistantMessage({ message, messages, isRunning, isLatest, onThumbsUp, onThumbsDown, onReadAloud, onRegenerate, additionalToolbarItems, toolbarVisible = true, markdownRenderer, toolbar, copyButton, inspectorButton, thumbsUpButton, thumbsDownButton, readAloudButton, regenerateButton, toolCallsView, children, className, ...props }) {
7458
+ /** Whether focus came from the keyboard; a pointer click also focuses buttons. */
7459
+ function isFocusVisible(element) {
7460
+ try {
7461
+ return element.matches(":focus-visible");
7462
+ } catch {
7463
+ return false;
7464
+ }
7465
+ }
7466
+ const HIDDEN_TOO = {
7467
+ checkVisibilityCSS: true,
7468
+ visibilityProperty: true
7469
+ };
7470
+ const FOCUSABLE = [
7471
+ "a[href]",
7472
+ "button:not([disabled])",
7473
+ "input:not([disabled])",
7474
+ "select:not([disabled])",
7475
+ "textarea:not([disabled])",
7476
+ "[tabindex]"
7477
+ ].map((selector) => `${selector}:not([tabindex="-1"]):not([data-radix-focus-guard])`).join(", ");
7478
+ /** Approximately the element Tab reaches after `element`, outside `skip`. */
7479
+ function nextFocusable(element, skip) {
7480
+ const all = Array.from(element.ownerDocument.querySelectorAll(FOCUSABLE)).filter((candidate) => candidate === element || !skip?.contains(candidate) && !candidate.closest("[hidden], [inert]"));
7481
+ const index = all.indexOf(element);
7482
+ return index === -1 ? null : all[index + 1] ?? null;
7483
+ }
7484
+ /**
7485
+ * The comment popover of a rated message, pointing at the chosen thumb. Kept
7486
+ * apart so only it, not the whole message, follows chat configuration.
7487
+ *
7488
+ * The popover lives in a portal, so it keeps keyboard order by hand: Shift+Tab
7489
+ * from its start returns to the thumb, Tab from its end moves on to the
7490
+ * control after the thumb, and Enter or Escape inside it returns to the thumb.
7491
+ */
7492
+ function FeedbackCommentPopover({ feedbackComment, comment, savedComment, rating, anchor, messageId }) {
7493
+ const labels = useCopilotChatConfiguration()?.labels ?? CopilotChatDefaultLabels;
7494
+ const contentRef = (0, react.useRef)(null);
7495
+ const focusInside = () => contentRef.current?.contains(document.activeElement) ?? false;
7496
+ const { focusRequest, takeFocusRequest, inputRef } = comment;
7497
+ const focusField = (0, react.useCallback)(() => {
7498
+ if (inputRef.current && takeFocusRequest()) inputRef.current.focus();
7499
+ }, [inputRef, takeFocusRequest]);
7500
+ (0, react.useEffect)(() => focusField(), [focusRequest, focusField]);
7501
+ /** Whether `target` is a thumb of this message, whose click decides itself. */
7502
+ const isOwnThumb = (target) => {
7503
+ return (target instanceof Element ? target.closest("[data-copilotkit-rating]") : null)?.closest("[data-message-id]")?.getAttribute("data-message-id") === messageId;
7504
+ };
7505
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Popover, {
7506
+ open: comment.open,
7507
+ onOpenChange: (open) => {
7508
+ if (!open) comment.close();
7509
+ },
7510
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PopoverAnchor, { virtualRef: anchor }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PopoverContent, {
7511
+ ref: contentRef,
7512
+ side: "bottom",
7513
+ collisionPadding: 8,
7514
+ hideWhenDetached: true,
7515
+ "aria-label": labels.assistantMessageFeedbackCommentPlaceholder,
7516
+ "data-copilotkit-ignore": true,
7517
+ onOpenAutoFocus: (event) => {
7518
+ event.preventDefault();
7519
+ focusField();
7520
+ },
7521
+ onCloseAutoFocus: (event) => event.preventDefault(),
7522
+ onInteractOutside: (event) => {
7523
+ if (isOwnThumb(event.target)) event.preventDefault();
7524
+ },
7525
+ onEscapeKeyDown: (event) => {
7526
+ const returnFocus = focusInside();
7527
+ if (returnFocus) event.stopPropagation();
7528
+ comment.close({ save: false });
7529
+ if (returnFocus) comment.returnFocus(anchor.current);
7530
+ },
7531
+ onKeyDown: (event) => {
7532
+ if (event.key !== "Tab") return;
7533
+ const focusables = Array.from(contentRef.current?.querySelectorAll(FOCUSABLE) ?? []);
7534
+ if (!(event.shiftKey ? document.activeElement === focusables[0] : document.activeElement === focusables[focusables.length - 1]) || !anchor.current?.isConnected) return;
7535
+ event.preventDefault();
7536
+ const target = event.shiftKey ? anchor.current : nextFocusable(anchor.current, contentRef.current) ?? anchor.current;
7537
+ comment.close();
7538
+ if (target === anchor.current) comment.returnFocus(target);
7539
+ else target.focus();
7540
+ },
7541
+ onPointerEnter: () => comment.enter("pointer"),
7542
+ onPointerLeave: () => comment.leave("pointer"),
7543
+ className: "cpk:z-[1300]",
7544
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Fragment, { children: renderSlot(feedbackComment, CopilotChatAssistantMessage.FeedbackComment, {
7545
+ value: comment.draft,
7546
+ savedComment,
7547
+ inputRef: comment.inputRef,
7548
+ onChange: comment.setDraft,
7549
+ onSubmit: () => {
7550
+ comment.close();
7551
+ comment.returnFocus(anchor.current);
7552
+ },
7553
+ onEditingChange: comment.setEditing,
7554
+ onLeave: (next) => {
7555
+ if (next && (contentRef.current?.contains(next) || isOwnThumb(next))) return;
7556
+ comment.leave();
7557
+ }
7558
+ }) }, rating)
7559
+ })]
7560
+ });
7561
+ }
7562
+ function CopilotChatAssistantMessage({ message, messages, isRunning, isLatest, onThumbsUp, onThumbsDown, onReadAloud, onRegenerate, additionalToolbarItems, toolbarVisible = true, markdownRenderer, toolbar, copyButton, inspectorButton, thumbsUpButton, thumbsDownButton, readAloudButton, regenerateButton, feedbackComment: feedbackCommentSlot, toolCallsView, children, className, ...props }) {
6432
7563
  useKatexStyles();
6433
7564
  const { isInspectorEnabled, openInspector } = useCopilotKitInspector();
6434
7565
  const shortcutsHidden = useInspectorShortcutsHidden();
6435
7566
  const showInspectorShortcut = isInspectorEnabled && !shortcutsHidden;
7567
+ const feedback = useMessageFeedback(message);
7568
+ const comment = useFeedbackComment(feedback);
7569
+ const rating = feedback?.feedback?.rating;
7570
+ const root = (0, react.useRef)(null);
7571
+ const commentAnchor = (0, react.useRef)(null);
7572
+ const commentRating = (0, react.useRef)(rating);
7573
+ (0, react.useLayoutEffect)(() => {
7574
+ if (comment.open && !commentAnchor.current?.isConnected && rating) commentAnchor.current = root.current?.querySelector(`[data-copilotkit-rating="${rating}"]`) ?? root.current?.querySelector("button[aria-pressed=\"true\"]") ?? null;
7575
+ });
7576
+ /**
7577
+ * Thumbs are toggles: a new rating is saved and opens its comment, and
7578
+ * clicking the chosen thumb again removes the rating. A keyboard click
7579
+ * (`detail` 0) moves focus into the comment, as there is no pointer.
7580
+ * Custom thumb slots may call `onClick` without an event.
7581
+ *
7582
+ * @returns Whether the app's handler should run: true for a new rating and
7583
+ * whenever this chat does not record ratings, false when one is removed.
7584
+ */
7585
+ const rate = (value, event) => {
7586
+ if (!feedback) return true;
7587
+ if (rating === value) {
7588
+ comment.close({ save: false });
7589
+ feedback.clear();
7590
+ return false;
7591
+ }
7592
+ feedback.rate(value);
7593
+ commentRating.current = value;
7594
+ commentAnchor.current = event?.currentTarget ?? null;
7595
+ comment.show({
7596
+ comment: "",
7597
+ focus: event?.detail === 0
7598
+ });
7599
+ return true;
7600
+ };
7601
+ /** Props that let the chosen thumb open and point at its comment. */
7602
+ const thumbFeedbackProps = (value) => {
7603
+ if (!feedback) return {};
7604
+ const thumb = { "data-copilotkit-rating": value };
7605
+ if (rating !== value) return {
7606
+ ...thumb,
7607
+ "aria-pressed": false
7608
+ };
7609
+ const enter = (target, via) => {
7610
+ if (!comment.open) commentRating.current = value;
7611
+ commentAnchor.current = target;
7612
+ comment.enter(via);
7613
+ };
7614
+ return {
7615
+ ...thumb,
7616
+ "aria-pressed": true,
7617
+ "aria-haspopup": "dialog",
7618
+ "aria-expanded": comment.open,
7619
+ onPointerEnter: (event) => {
7620
+ if (event.pointerType !== "touch") enter(event.currentTarget, "pointer");
7621
+ },
7622
+ onPointerLeave: (event) => {
7623
+ if (event.pointerType !== "touch") comment.leave("pointer");
7624
+ },
7625
+ onFocus: (event) => {
7626
+ if (isFocusVisible(event.currentTarget)) enter(event.currentTarget, "focus");
7627
+ },
7628
+ onBlur: () => comment.leave(),
7629
+ onKeyDown: (event) => {
7630
+ const input = comment.inputRef.current;
7631
+ if (event.key === "Tab" && !event.shiftKey && comment.open && input && input.checkVisibility?.(HIDDEN_TOO) !== false) {
7632
+ event.preventDefault();
7633
+ input.focus();
7634
+ }
7635
+ }
7636
+ };
7637
+ };
6436
7638
  const boundMarkdownRenderer = renderSlot(markdownRenderer, CopilotChatAssistantMessage.MarkdownRenderer, { content: message.content || "" });
6437
7639
  const boundCopyButton = renderSlot(copyButton, CopilotChatAssistantMessage.CopyButton, { onClick: async () => {
6438
7640
  if (message.content) return await (0, _copilotkit_shared.copyToClipboard)(message.content);
6439
7641
  return false;
6440
7642
  } });
6441
- const boundThumbsUpButton = renderSlot(thumbsUpButton, CopilotChatAssistantMessage.ThumbsUpButton, { onClick: onThumbsUp ? () => onThumbsUp(message) : void 0 });
7643
+ const boundThumbsUpButton = renderSlot(thumbsUpButton, CopilotChatAssistantMessage.ThumbsUpButton, {
7644
+ onClick: onThumbsUp || feedback ? (event) => {
7645
+ if (rate("up", event)) onThumbsUp?.(message);
7646
+ } : void 0,
7647
+ ...thumbFeedbackProps("up")
7648
+ });
6442
7649
  const boundInspectorButton = showInspectorShortcut ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BoundInspectorButton, {
6443
7650
  inspectorButton,
6444
7651
  messageId: message.id,
6445
7652
  openInspector
6446
7653
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, {});
6447
- const boundThumbsDownButton = renderSlot(thumbsDownButton, CopilotChatAssistantMessage.ThumbsDownButton, { onClick: onThumbsDown ? () => onThumbsDown(message) : void 0 });
7654
+ const boundThumbsDownButton = renderSlot(thumbsDownButton, CopilotChatAssistantMessage.ThumbsDownButton, {
7655
+ onClick: onThumbsDown || feedback ? (event) => {
7656
+ if (rate("down", event)) onThumbsDown?.(message);
7657
+ } : void 0,
7658
+ ...thumbFeedbackProps("down")
7659
+ });
6448
7660
  const boundReadAloudButton = renderSlot(readAloudButton, CopilotChatAssistantMessage.ReadAloudButton, { onClick: onReadAloud ? () => onReadAloud(message) : void 0 });
6449
7661
  const boundRegenerateButton = renderSlot(regenerateButton, CopilotChatAssistantMessage.RegenerateButton, { onClick: onRegenerate ? () => onRegenerate(message) : void 0 });
6450
7662
  const boundToolbar = renderSlot(toolbar, CopilotChatAssistantMessage.Toolbar, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6451
7663
  className: "cpk:flex cpk:w-full cpk:items-center cpk:gap-1",
6452
7664
  children: [
6453
7665
  boundCopyButton,
6454
- (onThumbsUp || thumbsUpButton) && boundThumbsUpButton,
6455
- (onThumbsDown || thumbsDownButton) && boundThumbsDownButton,
7666
+ (onThumbsUp || thumbsUpButton || feedback) && boundThumbsUpButton,
7667
+ (onThumbsDown || thumbsDownButton || feedback) && boundThumbsDownButton,
6456
7668
  (onReadAloud || readAloudButton) && boundReadAloudButton,
6457
7669
  (onRegenerate || regenerateButton) && boundRegenerateButton,
6458
7670
  additionalToolbarItems,
@@ -6466,11 +7678,31 @@ function CopilotChatAssistantMessage({ message, messages, isRunning, isLatest, o
6466
7678
  const hasContent = !!(message.content && message.content.trim().length > 0);
6467
7679
  const isLatestAssistantMessage = message.role === "assistant" && (isLatest ?? messages?.[messages.length - 1]?.id === message.id);
6468
7680
  const shouldShowToolbar = toolbarVisible && (hasContent || showInspectorShortcut) && !(isRunning && isLatestAssistantMessage);
6469
- if (children) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7681
+ const { open: commentOpen, close: closeComment } = comment;
7682
+ (0, react.useEffect)(() => {
7683
+ if (!commentOpen) return;
7684
+ if (rating !== commentRating.current) closeComment({ save: false });
7685
+ else if (!shouldShowToolbar) closeComment();
7686
+ }, [
7687
+ commentOpen,
7688
+ shouldShowToolbar,
7689
+ rating,
7690
+ closeComment
7691
+ ]);
7692
+ const feedbackComment = feedback?.feedback && shouldShowToolbar && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FeedbackCommentPopover, {
7693
+ feedbackComment: feedbackCommentSlot,
7694
+ comment,
7695
+ savedComment: feedback.feedback.comment ?? "",
7696
+ rating: feedback.feedback.rating,
7697
+ anchor: commentAnchor,
7698
+ messageId: message.id
7699
+ });
7700
+ if (children) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7701
+ ref: root,
6470
7702
  "data-copilotkit": true,
6471
7703
  "data-message-id": message.id,
6472
7704
  style: { display: "contents" },
6473
- children: children({
7705
+ children: [children({
6474
7706
  markdownRenderer: boundMarkdownRenderer,
6475
7707
  toolbar: boundToolbar,
6476
7708
  toolCallsView: boundToolCallsView,
@@ -6489,21 +7721,26 @@ function CopilotChatAssistantMessage({ message, messages, isRunning, isLatest, o
6489
7721
  onRegenerate,
6490
7722
  additionalToolbarItems,
6491
7723
  toolbarVisible: shouldShowToolbar
6492
- })
7724
+ }), feedbackComment]
6493
7725
  });
6494
7726
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7727
+ ref: root,
6495
7728
  "data-copilotkit": true,
6496
7729
  "data-testid": "copilot-assistant-message",
6497
7730
  className: (0, tailwind_merge.twMerge)("copilotKitMessage copilotKitAssistantMessage", className),
6498
7731
  ...props,
6499
7732
  "data-message-id": message.id,
6500
7733
  children: [
6501
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6502
- className: "cpk:prose cpk:max-w-full cpk:break-words cpk:dark:prose-invert",
6503
- children: boundMarkdownRenderer
7734
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkText, {
7735
+ messageId: message.id,
7736
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7737
+ className: "cpk:prose cpk:max-w-full cpk:break-words cpk:dark:prose-invert",
7738
+ children: boundMarkdownRenderer
7739
+ })
6504
7740
  }),
6505
7741
  boundToolCallsView,
6506
- shouldShowToolbar && boundToolbar
7742
+ shouldShowToolbar && boundToolbar,
7743
+ feedbackComment
6507
7744
  ]
6508
7745
  });
6509
7746
  }
@@ -6519,20 +7756,27 @@ function CopilotChatAssistantMessage({ message, messages, isRunning, isLatest, o
6519
7756
  ...props
6520
7757
  });
6521
7758
  const ToolbarButton = _CopilotChatAssistantMessage.ToolbarButton = ({ title, tooltip, tooltipClassName, children, ...props }) => {
6522
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
6523
- asChild: true,
6524
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
6525
- type: "button",
6526
- variant: "assistantMessageToolbarButton",
6527
- "aria-label": title,
6528
- ...props,
6529
- children
6530
- })
6531
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, {
6532
- side: "bottom",
6533
- className: tooltipClassName,
6534
- children: tooltip ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: title })
6535
- })] });
7759
+ const [tooltipOpen, setTooltipOpen] = (0, react.useState)(false);
7760
+ const popupOpen = props["aria-expanded"] === true;
7761
+ if (popupOpen && tooltipOpen) setTooltipOpen(false);
7762
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, {
7763
+ open: tooltipOpen && !popupOpen,
7764
+ onOpenChange: (open) => setTooltipOpen(open && !popupOpen),
7765
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
7766
+ asChild: true,
7767
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
7768
+ type: "button",
7769
+ variant: "assistantMessageToolbarButton",
7770
+ "aria-label": title,
7771
+ ...props,
7772
+ children
7773
+ })
7774
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, {
7775
+ side: "bottom",
7776
+ className: tooltipClassName,
7777
+ children: tooltip ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: title })
7778
+ })]
7779
+ });
6536
7780
  };
6537
7781
  _CopilotChatAssistantMessage.CopyButton = ({ className, title, onClick, ...props }) => {
6538
7782
  const labels = useCopilotChatConfiguration()?.labels ?? CopilotChatDefaultLabels;
@@ -6571,7 +7815,10 @@ function CopilotChatAssistantMessage({ message, messages, isRunning, isLatest, o
6571
7815
  "data-testid": "copilot-thumbs-up-button",
6572
7816
  title: title || labels.assistantMessageToolbarThumbsUpLabel,
6573
7817
  ...props,
6574
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ThumbsUp, { className: "cpk:size-[18px]" })
7818
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ThumbsUp, {
7819
+ className: "cpk:size-[18px]",
7820
+ fill: props["aria-pressed"] === true ? "currentColor" : "none"
7821
+ })
6575
7822
  });
6576
7823
  };
6577
7824
  _CopilotChatAssistantMessage.ThumbsDownButton = ({ title, ...props }) => {
@@ -6580,7 +7827,45 @@ function CopilotChatAssistantMessage({ message, messages, isRunning, isLatest, o
6580
7827
  "data-testid": "copilot-thumbs-down-button",
6581
7828
  title: title || labels.assistantMessageToolbarThumbsDownLabel,
6582
7829
  ...props,
6583
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ThumbsDown, { className: "cpk:size-[18px]" })
7830
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ThumbsDown, {
7831
+ className: "cpk:size-[18px]",
7832
+ fill: props["aria-pressed"] === true ? "currentColor" : "none"
7833
+ })
7834
+ });
7835
+ };
7836
+ _CopilotChatAssistantMessage.FeedbackComment = ({ value, savedComment, inputRef, onChange, onSubmit, onEditingChange, onLeave, className }) => {
7837
+ const labels = useCopilotChatConfiguration()?.labels ?? CopilotChatDefaultLabels;
7838
+ const changed = value.trim() !== savedComment;
7839
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
7840
+ "data-testid": "copilot-feedback-comment",
7841
+ className: (0, tailwind_merge.twMerge)("cpk:flex cpk:w-80 cpk:max-w-[calc(100vw-1rem)] cpk:items-center cpk:gap-1 cpk:rounded-full cpk:border cpk:border-border cpk:bg-background cpk:py-0.5 cpk:pl-3 cpk:pr-0.5 cpk:text-sm cpk:shadow-md", className),
7842
+ onSubmit: (event) => {
7843
+ event.preventDefault();
7844
+ onSubmit();
7845
+ },
7846
+ onFocus: () => onEditingChange?.(true),
7847
+ onBlur: (event) => {
7848
+ const next = event.relatedTarget instanceof Element ? event.relatedTarget : null;
7849
+ if (event.currentTarget.contains(next)) return;
7850
+ if (typeof document !== "undefined" && !document.hasFocus()) return;
7851
+ onEditingChange?.(false);
7852
+ onLeave?.(next);
7853
+ },
7854
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
7855
+ ref: inputRef,
7856
+ value,
7857
+ maxLength: MESSAGE_FEEDBACK_MAX_COMMENT_LENGTH,
7858
+ onChange: (event) => onChange(event.target.value),
7859
+ placeholder: labels.assistantMessageFeedbackCommentPlaceholder,
7860
+ "aria-label": labels.assistantMessageFeedbackCommentPlaceholder,
7861
+ className: "cpk:min-w-0 cpk:flex-1 cpk:bg-transparent cpk:py-1 cpk:text-foreground cpk:outline-none cpk:placeholder:text-muted-foreground"
7862
+ }), changed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
7863
+ type: "submit",
7864
+ variant: "assistantMessageToolbarButton",
7865
+ "aria-label": labels.assistantMessageFeedbackCommentSubmitLabel,
7866
+ title: labels.assistantMessageFeedbackCommentSubmitLabel,
7867
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ArrowUp, { className: "cpk:size-[16px]" })
7868
+ })]
6584
7869
  });
6585
7870
  };
6586
7871
  _CopilotChatAssistantMessage.ReadAloudButton = ({ title, ...props }) => {
@@ -6608,6 +7893,7 @@ CopilotChatAssistantMessage.CopyButton.displayName = "CopilotChatAssistantMessag
6608
7893
  CopilotChatAssistantMessage.InspectorButton.displayName = "CopilotChatAssistantMessage.InspectorButton";
6609
7894
  CopilotChatAssistantMessage.ThumbsUpButton.displayName = "CopilotChatAssistantMessage.ThumbsUpButton";
6610
7895
  CopilotChatAssistantMessage.ThumbsDownButton.displayName = "CopilotChatAssistantMessage.ThumbsDownButton";
7896
+ CopilotChatAssistantMessage.FeedbackComment.displayName = "CopilotChatAssistantMessage.FeedbackComment";
6611
7897
  CopilotChatAssistantMessage.ReadAloudButton.displayName = "CopilotChatAssistantMessage.ReadAloudButton";
6612
7898
  CopilotChatAssistantMessage.RegenerateButton.displayName = "CopilotChatAssistantMessage.RegenerateButton";
6613
7899
  var CopilotChatAssistantMessage_default = CopilotChatAssistantMessage;
@@ -6913,6 +8199,7 @@ function CopilotChatUserMessage({ message, onEditMessage, branchIndex, numberOfB
6913
8199
  });
6914
8200
  };
6915
8201
  _CopilotChatUserMessage.BranchNavigation = ({ className, currentBranch = 0, numberOfBranches = 1, onSwitchToBranch, message, ...props }) => {
8202
+ const labels = useCopilotChatConfiguration()?.labels ?? CopilotChatDefaultLabels;
6916
8203
  if (!numberOfBranches || numberOfBranches <= 1 || !onSwitchToBranch) return null;
6917
8204
  const canGoPrev = currentBranch > 0;
6918
8205
  const canGoNext = currentBranch < numberOfBranches - 1;
@@ -6931,6 +8218,7 @@ function CopilotChatUserMessage({ message, onEditMessage, branchIndex, numberOfB
6931
8218
  }),
6932
8219
  disabled: !canGoPrev,
6933
8220
  className: "cpk:h-6 cpk:w-6 cpk:p-0",
8221
+ "aria-label": labels.userMessageToolbarPreviousBranchLabel,
6934
8222
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronLeft, { className: "cpk:size-[20px]" })
6935
8223
  }),
6936
8224
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -6951,6 +8239,7 @@ function CopilotChatUserMessage({ message, onEditMessage, branchIndex, numberOfB
6951
8239
  }),
6952
8240
  disabled: !canGoNext,
6953
8241
  className: "cpk:h-6 cpk:w-6 cpk:p-0",
8242
+ "aria-label": labels.userMessageToolbarNextBranchLabel,
6954
8243
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, { className: "cpk:size-[20px]" })
6955
8244
  })
6956
8245
  ]
@@ -6971,7 +8260,7 @@ var CopilotChatUserMessage_default = CopilotChatUserMessage;
6971
8260
  /**
6972
8261
  * Formats an elapsed duration (in seconds) to a human-readable string.
6973
8262
  */
6974
- function formatDuration(seconds) {
8263
+ function formatDuration$1(seconds) {
6975
8264
  if (seconds < 1) return "a few seconds";
6976
8265
  if (seconds < 60) return `${Math.round(seconds)} seconds`;
6977
8266
  const mins = Math.floor(seconds / 60);
@@ -7009,7 +8298,7 @@ function CopilotChatReasoningMessage({ message, messages, isRunning, isLatest: i
7009
8298
  userToggledRef.current = true;
7010
8299
  setIsOpen((prev) => !prev);
7011
8300
  } : void 0;
7012
- const label = isStreaming ? "Thinking…" : `Thought for ${formatDuration(elapsed)}`;
8301
+ const label = isStreaming ? "Thinking…" : `Thought for ${formatDuration$1(elapsed)}`;
7013
8302
  const boundHeader = renderSlot(header, CopilotChatReasoningMessage.Header, {
7014
8303
  isOpen,
7015
8304
  label,
@@ -7231,6 +8520,71 @@ const ScrollElementContext = react.default.createContext(null);
7231
8520
  */
7232
8521
  const ScrollPinnedContext = react.default.createContext(false);
7233
8522
 
8523
+ //#endregion
8524
+ //#region src/v2/components/chat/CopilotChatWorkSummary.tsx
8525
+ /** "45s", "1m 5s", "2m". Used for the live timer and the final duration. */
8526
+ function formatDuration(ms) {
8527
+ const seconds = Math.max(0, Math.floor(ms / 1e3));
8528
+ if (seconds < 60) return `${seconds}s`;
8529
+ const minutes = Math.floor(seconds / 60);
8530
+ const rest = seconds % 60;
8531
+ return rest ? `${minutes}m ${rest}s` : `${minutes}m`;
8532
+ }
8533
+ /** Re-renders every second while `active`, returning the current time. */
8534
+ function useNow(active) {
8535
+ const [now, setNow] = (0, react.useState)(() => Date.now());
8536
+ (0, react.useEffect)(() => {
8537
+ if (!active) return;
8538
+ setNow(Date.now());
8539
+ const timer = setInterval(() => setNow(Date.now()), 1e3);
8540
+ return () => clearInterval(timer);
8541
+ }, [active]);
8542
+ return now;
8543
+ }
8544
+ /**
8545
+ * The line at the top of a reply in the `"turn"` tool-call display. While the
8546
+ * agent works it reads "Working 4s"; once the reply is done it reads "Worked
8547
+ * for 9s · 7 tools" and toggles the work it folded away.
8548
+ */
8549
+ function CopilotChatWorkSummary({ isWorking, isOpen, calls, elapsedMs, workingSince, className, ...props }) {
8550
+ const now = useNow(isWorking);
8551
+ const failed = calls.filter((call) => call.toolMessage?.error).length;
8552
+ const liveMs = elapsedMs !== void 0 && workingSince !== void 0 ? elapsedMs + Math.max(0, now - workingSince) : void 0;
8553
+ const duration = elapsedMs !== void 0 ? formatDuration(Math.max(1e3, elapsedMs)) : void 0;
8554
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
8555
+ type: "button",
8556
+ "data-copilotkit": true,
8557
+ "data-testid": "copilot-work-summary",
8558
+ "data-state": isWorking ? "working" : "done",
8559
+ "aria-expanded": isOpen,
8560
+ className: (0, tailwind_merge.twMerge)("cpk:m-0 cpk:inline-flex cpk:max-w-full cpk:cursor-pointer cpk:items-center cpk:gap-1.5 cpk:rounded-md cpk:border-none cpk:bg-transparent cpk:p-0 cpk:py-0.5 cpk:text-sm cpk:text-muted-foreground cpk:transition-colors cpk:hover:text-foreground cpk:focus-visible:outline-2 cpk:focus-visible:outline-ring", className),
8561
+ ...props,
8562
+ children: [isWorking ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8563
+ className: "cpk:font-medium cpk-shimmer",
8564
+ children: "Working"
8565
+ }), liveMs !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8566
+ className: "cpk:text-xs cpk:tabular-nums cpk:opacity-70",
8567
+ children: formatDuration(liveMs)
8568
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
8569
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8570
+ className: "cpk:font-medium",
8571
+ children: duration ? `Worked for ${duration}` : `Used ${pluralize(calls.length, "tool")}`
8572
+ }),
8573
+ duration && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8574
+ className: "cpk:text-xs cpk:opacity-70",
8575
+ children: pluralize(calls.length, "tool")
8576
+ }),
8577
+ failed > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
8578
+ className: "cpk:text-destructive",
8579
+ children: [failed, " failed"]
8580
+ })
8581
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, {
8582
+ "aria-hidden": "true",
8583
+ className: (0, tailwind_merge.twMerge)("cpk:size-3.5 cpk:shrink-0 cpk:transition-transform cpk:duration-200", isOpen && "cpk:rotate-90")
8584
+ })]
8585
+ });
8586
+ }
8587
+
7234
8588
  //#endregion
7235
8589
  //#region src/v2/components/intelligence-indicator/IntelligenceIndicatorView.tsx
7236
8590
  /**
@@ -7717,7 +9071,7 @@ function messageGroup(group) {
7717
9071
  }
7718
9072
  const GROUP_ROW_KEY_PREFIX = "copilotkit-group:";
7719
9073
  const VIRTUALIZE_THRESHOLD = 50;
7720
- function CopilotChatMessageView({ messages = [], assistantMessage, userMessage, reasoningMessage, cursor, intelligenceIndicator, isRunning = false, transformMessages, groupMessages, children, className, ...props }) {
9074
+ function CopilotChatMessageView({ messages = [], assistantMessage, userMessage, reasoningMessage, cursor, intelligenceIndicator, workSummary, isRunning = false, transformMessages, groupMessages, messageFeedback = true, toolCallDisplay = "grouped", children, className, ...props }) {
7721
9075
  const isPinnedToBottom = (0, react.useContext)(ScrollPinnedContext);
7722
9076
  const renderCustomMessage = useRenderCustomMessages();
7723
9077
  const { renderActivityMessage } = useRenderActivityMessage();
@@ -7758,6 +9112,100 @@ function CopilotChatMessageView({ messages = [], assistantMessage, userMessage,
7758
9112
  transformedMessages
7759
9113
  ]);
7760
9114
  const latestRenderedId = renderedMessages[renderedMessages.length - 1]?.id;
9115
+ const rendererNames = useToolCallRendererNames();
9116
+ const toolCallLayouts = (0, react.useMemo)(() => toolCallDisplay === "separate" ? /* @__PURE__ */ new Map() : getToolCallLayouts(renderedMessages, messages, rendererNames), [
9117
+ toolCallDisplay,
9118
+ renderedMessages,
9119
+ messages,
9120
+ rendererNames
9121
+ ]);
9122
+ const turnWork = (0, react.useMemo)(() => toolCallDisplay === "turn" ? getTurnWork(renderedMessages, toolCallLayouts) : void 0, [
9123
+ toolCallDisplay,
9124
+ renderedMessages,
9125
+ toolCallLayouts
9126
+ ]);
9127
+ const turnByFirstMessage = (0, react.useMemo)(() => {
9128
+ const byFirst = /* @__PURE__ */ new Map();
9129
+ for (const turn of turnWork?.values() ?? []) byFirst.set(turn.firstMessageId, turn);
9130
+ return byFirst;
9131
+ }, [turnWork]);
9132
+ let latestTurnKey = "start";
9133
+ for (let i = renderedMessages.length - 1; i >= 0; i--) if (renderedMessages[i].role === "user") {
9134
+ latestTurnKey = renderedMessages[i].id;
9135
+ break;
9136
+ }
9137
+ const isWorkingOn = (turnKey) => isRunning && turnKey === latestTurnKey;
9138
+ const [toggledTurns, setToggledTurns] = (0, react.useState)(() => /* @__PURE__ */ new Map());
9139
+ const isWorkOpen = (turnKey) => toggledTurns.get(turnKey) ?? isWorkingOn(turnKey);
9140
+ const workTimesRef = (0, react.useRef)(/* @__PURE__ */ new Map());
9141
+ if (toolCallDisplay === "turn") {
9142
+ const now = Date.now();
9143
+ const workTimes = workTimesRef.current;
9144
+ if (isRunning) {
9145
+ const times = workTimes.get(latestTurnKey);
9146
+ if (times) times.workingSince ??= now;
9147
+ else if (renderedMessages[renderedMessages.length - 1]?.role === "user") workTimes.set(latestTurnKey, {
9148
+ elapsedMs: 0,
9149
+ workingSince: now
9150
+ });
9151
+ } else for (const times of workTimes.values()) {
9152
+ if (times.workingSince === void 0) continue;
9153
+ times.elapsedMs += now - times.workingSince;
9154
+ times.workingSince = void 0;
9155
+ }
9156
+ }
9157
+ const { workOpen, workTextOpen } = (0, react.useMemo)(() => {
9158
+ const open = /* @__PURE__ */ new Map();
9159
+ const textOpen = /* @__PURE__ */ new Map();
9160
+ for (const turn of turnWork?.values() ?? []) {
9161
+ const turnOpen = toggledTurns.get(turn.key) ?? (isRunning && turn.key === latestTurnKey);
9162
+ for (const id of turn.workMessageIds) {
9163
+ open.set(id, turnOpen);
9164
+ if (id !== turn.replyTextMessageId) textOpen.set(id, turnOpen);
9165
+ }
9166
+ }
9167
+ return {
9168
+ workOpen: open,
9169
+ workTextOpen: textOpen
9170
+ };
9171
+ }, [
9172
+ turnWork,
9173
+ toggledTurns,
9174
+ isRunning,
9175
+ latestTurnKey
9176
+ ]);
9177
+ const toolCallDisplayContext = (0, react.useMemo)(() => ({
9178
+ display: toolCallDisplay,
9179
+ isRunning,
9180
+ layouts: toolCallLayouts,
9181
+ workOpen,
9182
+ workTextOpen
9183
+ }), [
9184
+ toolCallDisplay,
9185
+ isRunning,
9186
+ toolCallLayouts,
9187
+ workOpen,
9188
+ workTextOpen
9189
+ ]);
9190
+ const renderWorkSummary = (turnKey, calls) => {
9191
+ const working = isWorkingOn(turnKey);
9192
+ const open = isWorkOpen(turnKey);
9193
+ const times = workTimesRef.current.get(turnKey);
9194
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Collapsible, {
9195
+ open: working || calls.length > 0,
9196
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9197
+ className: "cpk:pt-1 cpk:pb-2",
9198
+ children: renderSlot(workSummary, CopilotChatWorkSummary, {
9199
+ isWorking: working,
9200
+ isOpen: open,
9201
+ calls,
9202
+ elapsedMs: times?.elapsedMs,
9203
+ workingSince: times?.workingSince,
9204
+ onClick: () => setToggledTurns((toggled) => new Map(toggled).set(turnKey, !open))
9205
+ })
9206
+ })
9207
+ }, `work-summary-${turnKey}`);
9208
+ };
7761
9209
  const transformDuplicateId = (0, react.useMemo)(() => {
7762
9210
  if (process.env.NODE_ENV === "production" || !transformMessages && !groupMessages) return;
7763
9211
  const seen = /* @__PURE__ */ new Set();
@@ -7823,6 +9271,18 @@ function CopilotChatMessageView({ messages = [], assistantMessage, userMessage,
7823
9271
  const { Component: AssistantComponent, slotProps: assistantSlotProps } = (0, react.useMemo)(() => resolveSlotComponent(assistantMessage, CopilotChatAssistantMessage_default), [assistantMessage]);
7824
9272
  const agentId = config?.agentId;
7825
9273
  const threadId = config?.threadId;
9274
+ const learningFeedbackEnabled = useLearningFeedbackEnabled(copilotkit);
9275
+ const feedbackScope = (0, react.useMemo)(() => messageFeedback && learningFeedbackEnabled && agentId !== void 0 && threadId !== void 0 ? {
9276
+ copilotkit,
9277
+ agentId,
9278
+ threadId
9279
+ } : null, [
9280
+ messageFeedback,
9281
+ learningFeedbackEnabled,
9282
+ copilotkit,
9283
+ agentId,
9284
+ threadId
9285
+ ]);
7826
9286
  const assistantSlotPropsWithFeedback = (0, react.useMemo)(() => {
7827
9287
  const onThumbsUp = assistantSlotProps?.onThumbsUp;
7828
9288
  const onThumbsDown = assistantSlotProps?.onThumbsDown;
@@ -7889,7 +9349,7 @@ function CopilotChatMessageView({ messages = [], assistantMessage, userMessage,
7889
9349
  measuredRef.current.total = total - (sizes.get(index) ?? 0) + height;
7890
9350
  sizes.set(index, height);
7891
9351
  }
7892
- return height;
9352
+ return height > 0 ? height : measuredRef.current.sizes.get(index) ?? 100;
7893
9353
  }, []);
7894
9354
  const isPinnedToBottomRef = react.default.useRef(isPinnedToBottom);
7895
9355
  const shouldAdjustScrollOnResize = react.default.useCallback((item, _delta, instance) => {
@@ -7899,7 +9359,7 @@ function CopilotChatMessageView({ messages = [], assistantMessage, userMessage,
7899
9359
  }, []);
7900
9360
  const virtualizer = (0, _tanstack_react_virtual.useVirtualizer)({
7901
9361
  count: shouldVirtualize ? rows.length : 0,
7902
- getScrollElement: () => scrollElement,
9362
+ getScrollElement: () => shouldVirtualize ? scrollElement : null,
7903
9363
  estimateSize: estimateRowSize,
7904
9364
  overscan: 5,
7905
9365
  measureElement: measureRowElement,
@@ -7910,8 +9370,11 @@ function CopilotChatMessageView({ messages = [], assistantMessage, userMessage,
7910
9370
  });
7911
9371
  isPinnedToBottomRef.current = isPinnedToBottom;
7912
9372
  virtualizer.shouldAdjustScrollPositionOnItemSizeChange = shouldAdjustScrollOnResize;
9373
+ const lastEndAlignedFirstMessageRef = react.default.useRef(void 0);
7913
9374
  (0, react.useLayoutEffect)(() => {
7914
9375
  if (!shouldVirtualize || !rows.length) return;
9376
+ if (lastEndAlignedFirstMessageRef.current === firstMessageId) return;
9377
+ lastEndAlignedFirstMessageRef.current = firstMessageId;
7915
9378
  virtualizer.scrollToIndex(rows.length - 1, { align: "end" });
7916
9379
  }, [shouldVirtualize, firstMessageId]);
7917
9380
  const intelligenceTurnAnchors = (0, react.useMemo)(() => getIntelligenceTurnAnchors(deduplicatedMessages, renderedMessages), [deduplicatedMessages, renderedMessages]);
@@ -7919,6 +9382,8 @@ function CopilotChatMessageView({ messages = [], assistantMessage, userMessage,
7919
9382
  const elements = [];
7920
9383
  const stateSnapshot = renderCustomMessage ? getStateSnapshotForMessage(message.id) : void 0;
7921
9384
  const rowKey = rowRenderKeys.get(message.id) ?? message.id;
9385
+ const turn = turnByFirstMessage.get(message.id);
9386
+ if (turn) elements.push(renderWorkSummary(turn.key, turn.calls));
7922
9387
  if (renderCustomMessage) elements.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemoizedCustomMessage, {
7923
9388
  message,
7924
9389
  position: "before",
@@ -7942,14 +9407,20 @@ function CopilotChatMessageView({ messages = [], assistantMessage, userMessage,
7942
9407
  message,
7943
9408
  renderActivityMessage
7944
9409
  }, rowKey));
7945
- else if (message.role === "reasoning") elements.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemoizedReasoningMessage, {
7946
- message,
7947
- messages,
7948
- isRunning,
7949
- isLatest: message.id === latestRenderedId,
7950
- ReasoningMessageComponent: ReasoningComponent,
7951
- slotProps: reasoningSlotProps
7952
- }, rowKey));
9410
+ else if (message.role === "reasoning") {
9411
+ const reasoning = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemoizedReasoningMessage, {
9412
+ message,
9413
+ messages,
9414
+ isRunning,
9415
+ isLatest: message.id === latestRenderedId,
9416
+ ReasoningMessageComponent: ReasoningComponent,
9417
+ slotProps: reasoningSlotProps
9418
+ }, rowKey);
9419
+ elements.push(toolCallDisplay === "turn" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Collapsible, {
9420
+ open: workOpen.get(message.id) ?? true,
9421
+ children: reasoning
9422
+ }, rowKey) : reasoning);
9423
+ }
7953
9424
  if (renderCustomMessage) elements.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemoizedCustomMessage, {
7954
9425
  message,
7955
9426
  position: "after",
@@ -7977,52 +9448,157 @@ function CopilotChatMessageView({ messages = [], assistantMessage, userMessage,
7977
9448
  children: row.messages.flatMap(renderMessageBlock)
7978
9449
  }, rowKey(row))];
7979
9450
  };
9451
+ const rowKeysRef = (0, react.useRef)([]);
9452
+ rowKeysRef.current = rows.map(rowKey);
9453
+ const virtualModeRef = (0, react.useRef)(shouldVirtualize);
9454
+ virtualModeRef.current = shouldVirtualize;
9455
+ (0, react.useEffect)(() => {
9456
+ const element = scrollElementFromCtx;
9457
+ if (!element) return;
9458
+ let anchor = null;
9459
+ let suspended = false;
9460
+ let frame = 0;
9461
+ const visibleRows = () => Array.from(element.querySelectorAll("[data-scroll-anchor]"));
9462
+ const remember = () => {
9463
+ if (suspended || element.clientHeight === 0 || !virtualModeRef.current) return;
9464
+ if (isPinnedToBottomRef.current) {
9465
+ anchor = null;
9466
+ return;
9467
+ }
9468
+ const viewport = element.getBoundingClientRect();
9469
+ const row = visibleRows().find((row) => {
9470
+ const bounds = row.getBoundingClientRect();
9471
+ return bounds.bottom > viewport.top && bounds.top < viewport.bottom;
9472
+ });
9473
+ if (row) anchor = {
9474
+ key: row.dataset.scrollAnchor,
9475
+ offset: row.getBoundingClientRect().top - viewport.top
9476
+ };
9477
+ };
9478
+ const observer = new ResizeObserver(() => {
9479
+ if (element.clientHeight === 0) {
9480
+ suspended = true;
9481
+ cancelAnimationFrame(frame);
9482
+ return;
9483
+ }
9484
+ if (!suspended || !anchor) {
9485
+ suspended = false;
9486
+ remember();
9487
+ return;
9488
+ }
9489
+ let attempts = 0;
9490
+ let stableFrames = 0;
9491
+ const restore = () => {
9492
+ if (!anchor || element.clientHeight === 0) return;
9493
+ const index = rowKeysRef.current.indexOf(anchor.key);
9494
+ if (index < 0) {
9495
+ anchor = null;
9496
+ suspended = false;
9497
+ return;
9498
+ }
9499
+ const row = visibleRows().find((row) => row.dataset.scrollAnchor === anchor.key);
9500
+ if (row) {
9501
+ const delta = row.getBoundingClientRect().top - element.getBoundingClientRect().top - anchor.offset;
9502
+ element.scrollTop += delta;
9503
+ stableFrames = Math.abs(delta) < 1 ? stableFrames + 1 : 0;
9504
+ } else {
9505
+ const offset = virtualizer.getOffsetForIndex(index, "start")?.[0];
9506
+ if (offset !== void 0) virtualizer.scrollToOffset(offset - anchor.offset);
9507
+ stableFrames = 0;
9508
+ }
9509
+ if (++attempts < 60 && stableFrames < 3) frame = requestAnimationFrame(restore);
9510
+ else {
9511
+ suspended = false;
9512
+ remember();
9513
+ }
9514
+ };
9515
+ frame = requestAnimationFrame(restore);
9516
+ });
9517
+ const interrupt = () => {
9518
+ cancelAnimationFrame(frame);
9519
+ anchor = null;
9520
+ suspended = false;
9521
+ };
9522
+ element.addEventListener("scroll", remember, { passive: true });
9523
+ element.addEventListener("wheel", interrupt, { passive: true });
9524
+ element.addEventListener("touchstart", interrupt, { passive: true });
9525
+ element.addEventListener("keydown", interrupt);
9526
+ observer.observe(element);
9527
+ remember();
9528
+ return () => {
9529
+ cancelAnimationFrame(frame);
9530
+ observer.disconnect();
9531
+ element.removeEventListener("scroll", remember);
9532
+ element.removeEventListener("wheel", interrupt);
9533
+ element.removeEventListener("touchstart", interrupt);
9534
+ element.removeEventListener("keydown", interrupt);
9535
+ };
9536
+ }, [
9537
+ scrollElementFromCtx,
9538
+ virtualizer,
9539
+ firstMessageId
9540
+ ]);
7980
9541
  const messageElements = shouldVirtualize ? [] : rows.flatMap(renderRow);
7981
- if (children) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7982
- "data-copilotkit": true,
7983
- style: { display: "contents" },
7984
- children: children({
7985
- messageElements,
7986
- messages,
7987
- isRunning,
7988
- interruptElement
9542
+ if (children) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageFeedbackScopeContext.Provider, {
9543
+ value: feedbackScope,
9544
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCallDisplayContext.Provider, {
9545
+ value: toolCallDisplayContext,
9546
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9547
+ "data-copilotkit": true,
9548
+ style: { display: "contents" },
9549
+ children: children({
9550
+ messageElements,
9551
+ messages,
9552
+ isRunning,
9553
+ interruptElement
9554
+ })
9555
+ })
7989
9556
  })
7990
9557
  });
7991
9558
  const lastMessage = renderedMessages[renderedMessages.length - 1];
7992
- const showCursor = isRunning && lastMessage?.role !== "reasoning";
7993
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7994
- "data-copilotkit": true,
7995
- "data-testid": "copilot-message-list",
7996
- className: (0, tailwind_merge.twMerge)("copilotKitMessages cpk:flex cpk:flex-col", className),
7997
- ...props,
7998
- children: [
7999
- shouldVirtualize ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8000
- style: {
8001
- height: virtualizer.getTotalSize(),
8002
- position: "relative"
8003
- },
8004
- children: virtualizer.getVirtualItems().map((virtualItem) => {
8005
- const row = rows[virtualItem.index];
8006
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8007
- "data-index": virtualItem.index,
8008
- ref: virtualizer.measureElement,
9559
+ const showCursor = isRunning && lastMessage?.role !== "reasoning" && toolCallDisplay !== "turn";
9560
+ const pendingWorkSummary = toolCallDisplay === "turn" && isRunning && lastMessage?.role === "user";
9561
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageFeedbackScopeContext.Provider, {
9562
+ value: feedbackScope,
9563
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCallDisplayContext.Provider, {
9564
+ value: toolCallDisplayContext,
9565
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9566
+ "data-copilotkit": true,
9567
+ "data-testid": "copilot-message-list",
9568
+ className: (0, tailwind_merge.twMerge)("copilotKitMessages cpk:flex cpk:flex-col", className),
9569
+ ...props,
9570
+ children: [
9571
+ shouldVirtualize ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8009
9572
  style: {
8010
- position: "absolute",
8011
- top: 0,
8012
- left: 0,
8013
- width: "100%",
8014
- transform: `translateY(${virtualItem.start}px)`
9573
+ height: virtualizer.getTotalSize(),
9574
+ position: "relative"
8015
9575
  },
8016
- children: renderRow(row)
8017
- }, rowKey(row));
8018
- })
8019
- }) : messageElements,
8020
- interruptElement,
8021
- showCursor && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8022
- className: "cpk:mt-2",
8023
- children: renderSlot(cursor, CopilotChatMessageView.Cursor, {})
9576
+ children: virtualizer.getVirtualItems().map((virtualItem) => {
9577
+ const row = rows[virtualItem.index];
9578
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9579
+ "data-index": virtualItem.index,
9580
+ "data-scroll-anchor": rowKey(row),
9581
+ ref: virtualizer.measureElement,
9582
+ style: {
9583
+ position: "absolute",
9584
+ top: 0,
9585
+ left: 0,
9586
+ width: "100%",
9587
+ transform: `translateY(${virtualItem.start}px)`
9588
+ },
9589
+ children: renderRow(row)
9590
+ }, rowKey(row));
9591
+ })
9592
+ }) : messageElements,
9593
+ interruptElement,
9594
+ pendingWorkSummary && renderWorkSummary(latestTurnKey, []),
9595
+ showCursor && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9596
+ className: "cpk:mt-2",
9597
+ children: renderSlot(cursor, CopilotChatMessageView.Cursor, {})
9598
+ })
9599
+ ]
8024
9600
  })
8025
- ]
9601
+ })
8026
9602
  });
8027
9603
  }
8028
9604
  CopilotChatMessageView.Cursor = function Cursor({ className, ...props }) {
@@ -8407,6 +9983,16 @@ function DropOverlay() {
8407
9983
  });
8408
9984
  }
8409
9985
  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, intelligenceIndicator, children, className, ...props }) {
9986
+ const configuredThreadId = useCopilotChatConfiguration()?.threadId;
9987
+ const [fallbackThreadId, setFallbackThreadId] = (0, react.useState)(messages[0]?.id);
9988
+ const previousMessageIds = (0, react.useRef)(/* @__PURE__ */ new Set());
9989
+ (0, react.useEffect)(() => {
9990
+ if (messages.length === 0) return;
9991
+ const overlaps = messages.some((message) => previousMessageIds.current.has(message.id));
9992
+ previousMessageIds.current = new Set(messages.map((message) => message.id));
9993
+ if (!overlaps) setFallbackThreadId(messages[0].id);
9994
+ }, [messages]);
9995
+ const scrollThreadId = configuredThreadId ?? fallbackThreadId;
8410
9996
  const [inputContainerEl, setInputContainerEl] = (0, react.useState)(null);
8411
9997
  const [inputContainerHeight, setInputContainerHeight] = (0, react.useState)(0);
8412
9998
  const [isResizing, setIsResizing] = (0, react.useState)(false);
@@ -8473,6 +10059,7 @@ function CopilotChatView({ messageView, input, scrollView, suggestionView, welco
8473
10059
  }) : null;
8474
10060
  const BoundScrollView = renderSlot(scrollView, CopilotChatView.ScrollView, {
8475
10061
  autoScroll,
10062
+ scrollThreadId,
8476
10063
  inputContainerHeight,
8477
10064
  isResizing,
8478
10065
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -8568,12 +10155,102 @@ function CopilotChatView({ messageView, input, scrollView, suggestionView, welco
8568
10155
  });
8569
10156
  }
8570
10157
  (function(_CopilotChatView) {
8571
- const ScrollContent = ({ children, scrollToBottomButton, feather, inputContainerHeight, isResizing }) => {
8572
- const { isAtBottom, scrollToBottom, scrollRef, state } = (0, use_stick_to_bottom.useStickToBottomContext)();
10158
+ const ScrollContent = ({ children, scrollToBottomButton, feather, inputContainerHeight, isResizing, scrollThreadId }) => {
10159
+ const { isAtBottom, scrollToBottom, stopScroll, scrollRef, state } = (0, use_stick_to_bottom.useStickToBottomContext)();
8573
10160
  const [scrollEl, setScrollEl] = (0, react.useState)(null);
10161
+ const readingOffsetRef = (0, react.useRef)(null);
10162
+ const pinIntentRef = (0, react.useRef)(false);
10163
+ const stateRef = (0, react.useRef)(state);
10164
+ stateRef.current = state;
8574
10165
  (0, react.useLayoutEffect)(() => {
8575
10166
  setScrollEl(scrollRef.current ?? null);
8576
10167
  }, []);
10168
+ (0, react.useEffect)(() => {
10169
+ if (!scrollEl) return;
10170
+ let hidden = false;
10171
+ let resumeBottom = false;
10172
+ let pendingFrame = 0;
10173
+ let resumeFrame = 0;
10174
+ let settleFrame = 0;
10175
+ readingOffsetRef.current = null;
10176
+ const rememberScroll = () => {
10177
+ if (hidden || scrollEl.clientHeight === 0) return;
10178
+ if (resumeBottom) {
10179
+ readingOffsetRef.current = null;
10180
+ return;
10181
+ }
10182
+ if (pinIntentRef.current) if (stateRef.current.escapedFromLock) pinIntentRef.current = false;
10183
+ else {
10184
+ readingOffsetRef.current = null;
10185
+ if (!stateRef.current.isNearBottom) return;
10186
+ pinIntentRef.current = false;
10187
+ }
10188
+ readingOffsetRef.current = !stateRef.current.isAtBottom && (stateRef.current.escapedFromLock || !stateRef.current.isNearBottom) ? scrollEl.scrollTop : null;
10189
+ };
10190
+ const onScroll = () => {
10191
+ rememberScroll();
10192
+ cancelAnimationFrame(pendingFrame);
10193
+ pendingFrame = requestAnimationFrame(rememberScroll);
10194
+ };
10195
+ const onWheel = (event) => {
10196
+ if (event.deltaY < 0) {
10197
+ pinIntentRef.current = false;
10198
+ resumeBottom = false;
10199
+ }
10200
+ };
10201
+ const resizeObserver = new ResizeObserver(() => {
10202
+ if (scrollEl.clientHeight === 0) {
10203
+ hidden = true;
10204
+ resumeBottom = pinIntentRef.current || readingOffsetRef.current === null;
10205
+ if (!resumeBottom) stopScroll();
10206
+ } else if (hidden) {
10207
+ hidden = false;
10208
+ if (resumeBottom) {
10209
+ stopScroll();
10210
+ resumeFrame = requestAnimationFrame(() => {
10211
+ settleFrame = requestAnimationFrame(() => {
10212
+ if (scrollEl.clientHeight > 0 && resumeBottom) {
10213
+ resumeBottom = false;
10214
+ pinIntentRef.current = true;
10215
+ scrollToBottom({ animation: "instant" });
10216
+ }
10217
+ });
10218
+ });
10219
+ } else if (readingOffsetRef.current !== null) {
10220
+ stopScroll();
10221
+ scrollEl.scrollTop = readingOffsetRef.current;
10222
+ }
10223
+ }
10224
+ });
10225
+ rememberScroll();
10226
+ scrollEl.addEventListener("scroll", onScroll);
10227
+ scrollEl.addEventListener("wheel", onWheel, { passive: true });
10228
+ resizeObserver.observe(scrollEl);
10229
+ return () => {
10230
+ scrollEl.removeEventListener("scroll", onScroll);
10231
+ scrollEl.removeEventListener("wheel", onWheel);
10232
+ cancelAnimationFrame(pendingFrame);
10233
+ cancelAnimationFrame(resumeFrame);
10234
+ cancelAnimationFrame(settleFrame);
10235
+ resizeObserver.disconnect();
10236
+ };
10237
+ }, [
10238
+ scrollEl,
10239
+ scrollToBottom,
10240
+ stopScroll,
10241
+ scrollThreadId
10242
+ ]);
10243
+ const startBottomPin = (0, react.useCallback)((instant = false) => {
10244
+ readingOffsetRef.current = null;
10245
+ pinIntentRef.current = true;
10246
+ scrollToBottom(instant ? { animation: "instant" } : void 0);
10247
+ }, [scrollToBottom]);
10248
+ const previousScrollThreadId = (0, react.useRef)(scrollThreadId);
10249
+ (0, react.useEffect)(() => {
10250
+ if (previousScrollThreadId.current === scrollThreadId) return;
10251
+ previousScrollThreadId.current = scrollThreadId;
10252
+ startBottomPin(true);
10253
+ }, [scrollThreadId, startBottomPin]);
8577
10254
  const BoundFeather = renderSlot(feather, CopilotChatView.Feather, {});
8578
10255
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScrollElementContext.Provider, {
8579
10256
  value: scrollEl,
@@ -8595,7 +10272,7 @@ function CopilotChatView({ messageView, input, scrollView, suggestionView, welco
8595
10272
  !isAtBottom && !isResizing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8596
10273
  className: "cpk:absolute cpk:inset-x-0 cpk:flex cpk:justify-center cpk:z-30 cpk:pointer-events-none",
8597
10274
  style: { bottom: `${inputContainerHeight + SCROLL_BUTTON_OFFSET}px` },
8598
- children: renderSlot(scrollToBottomButton, CopilotChatView.ScrollToBottomButton, { onClick: () => scrollToBottom() })
10275
+ children: renderSlot(scrollToBottomButton, CopilotChatView.ScrollToBottomButton, { onClick: () => startBottomPin() })
8599
10276
  })
8600
10277
  ] })
8601
10278
  })
@@ -8643,7 +10320,7 @@ function CopilotChatView({ messageView, input, scrollView, suggestionView, welco
8643
10320
  })
8644
10321
  });
8645
10322
  };
8646
- _CopilotChatView.ScrollView = ({ children, autoScroll = "pin-to-bottom", scrollToBottomButton, feather, inputContainerHeight = 0, isResizing = false, className, ...props }) => {
10323
+ _CopilotChatView.ScrollView = ({ children, autoScroll = "pin-to-bottom", scrollThreadId, scrollToBottomButton, feather, inputContainerHeight = 0, isResizing = false, className, ...props }) => {
8647
10324
  const mode = normalizeAutoScroll(autoScroll);
8648
10325
  const [hasMounted, setHasMounted] = (0, react.useState)(false);
8649
10326
  const scrollRef = (0, react.useRef)(null);
@@ -8732,6 +10409,7 @@ function CopilotChatView({ messageView, input, scrollView, suggestionView, welco
8732
10409
  initial: "smooth",
8733
10410
  ...props,
8734
10411
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScrollContent, {
10412
+ scrollThreadId,
8735
10413
  scrollToBottomButton,
8736
10414
  feather,
8737
10415
  inputContainerHeight,
@@ -8740,14 +10418,18 @@ function CopilotChatView({ messageView, input, scrollView, suggestionView, welco
8740
10418
  })
8741
10419
  });
8742
10420
  };
8743
- _CopilotChatView.ScrollToBottomButton = ({ className, ...props }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
8744
- "data-testid": "copilot-scroll-to-bottom",
8745
- variant: "outline",
8746
- size: "sm",
8747
- className: (0, tailwind_merge.twMerge)("cpk:rounded-full cpk:w-10 cpk:h-10 cpk:p-0 cpk:pointer-events-auto", "cpk:bg-white cpk:dark:bg-gray-900", "cpk:shadow-lg cpk:border cpk:border-gray-200 cpk:dark:border-gray-700", "cpk:hover:bg-gray-50 cpk:dark:hover:bg-gray-800", "cpk:flex cpk:items-center cpk:justify-center cpk:cursor-pointer", className),
8748
- ...props,
8749
- 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" })
8750
- });
10421
+ _CopilotChatView.ScrollToBottomButton = ({ className, ...props }) => {
10422
+ const labels = useCopilotChatConfiguration()?.labels ?? CopilotChatDefaultLabels;
10423
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
10424
+ "data-testid": "copilot-scroll-to-bottom",
10425
+ variant: "outline",
10426
+ size: "sm",
10427
+ className: (0, tailwind_merge.twMerge)("cpk:rounded-full cpk:w-10 cpk:h-10 cpk:p-0 cpk:pointer-events-auto", "cpk:bg-white cpk:dark:bg-gray-900", "cpk:shadow-lg cpk:border cpk:border-gray-200 cpk:dark:border-gray-700", "cpk:hover:bg-gray-50 cpk:dark:hover:bg-gray-800", "cpk:flex cpk:items-center cpk:justify-center cpk:cursor-pointer", className),
10428
+ "aria-label": labels.chatScrollToBottomLabel,
10429
+ ...props,
10430
+ 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" })
10431
+ });
10432
+ };
8751
10433
  _CopilotChatView.Feather = ({ className, ...props }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8752
10434
  className,
8753
10435
  ...props
@@ -10130,6 +11812,7 @@ function CopilotPopupViewInternal({ header, toggleButton, width, height, clickOu
10130
11812
  if (containerRef.current?.contains(target)) return;
10131
11813
  const toggleButton = document.querySelector("[data-slot='chat-toggle-button']");
10132
11814
  if (toggleButton && toggleButton.contains(target)) return;
11815
+ if (target instanceof Element && target.closest("[data-copilotkit-popover]")) return;
10133
11816
  setModalOpen?.(false);
10134
11817
  };
10135
11818
  document.addEventListener("pointerdown", handlePointerDown);
@@ -10640,62 +12323,14 @@ CopilotThreadsDrawer.displayName = "CopilotThreadsDrawer";
10640
12323
 
10641
12324
  //#endregion
10642
12325
  //#region src/v2/components/WildcardToolCallRender.tsx
12326
+ /**
12327
+ * Catch-all (`"*"`) tool-call renderer that shows CopilotKit's default
12328
+ * tool-call card for any tool without a dedicated renderer.
12329
+ */
10643
12330
  const WildcardToolCallRender = defineToolCallRenderer({
10644
12331
  name: "*",
10645
- render: ({ args, result, name, status }) => {
10646
- const [isExpanded, setIsExpanded] = (0, react.useState)(false);
10647
- const statusString = String(status);
10648
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
10649
- className: "cpk:mt-2 cpk:pb-2",
10650
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10651
- className: "cpk:rounded-xl cpk:border cpk:border-zinc-200/60 cpk:dark:border-zinc-800/60 cpk:bg-white/70 cpk:dark:bg-zinc-900/50 cpk:shadow-sm cpk:backdrop-blur cpk:p-4",
10652
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10653
- className: "cpk:flex cpk:items-center cpk:justify-between cpk:gap-3 cpk:cursor-pointer",
10654
- onClick: () => setIsExpanded(!isExpanded),
10655
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10656
- className: "cpk:flex cpk:items-center cpk:gap-2 cpk:min-w-0",
10657
- children: [
10658
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
10659
- className: `cpk:h-4 cpk:w-4 cpk:text-zinc-500 cpk:dark:text-zinc-400 cpk:transition-transform ${isExpanded ? "cpk:rotate-90" : ""}`,
10660
- fill: "none",
10661
- viewBox: "0 0 24 24",
10662
- strokeWidth: 2,
10663
- stroke: "currentColor",
10664
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
10665
- strokeLinecap: "round",
10666
- strokeLinejoin: "round",
10667
- d: "M8.25 4.5l7.5 7.5-7.5 7.5"
10668
- })
10669
- }),
10670
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "cpk:inline-block cpk:h-2 cpk:w-2 cpk:rounded-full cpk:bg-blue-500" }),
10671
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
10672
- className: "cpk:truncate cpk:text-sm cpk:font-medium cpk:text-zinc-900 cpk:dark:text-zinc-100",
10673
- children: name
10674
- })
10675
- ]
10676
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
10677
- className: `cpk:inline-flex cpk:items-center cpk:rounded-full cpk:px-2 cpk:py-1 cpk:text-xs cpk:font-medium ${statusString === "inProgress" || statusString === "executing" ? "cpk:bg-amber-100 cpk:text-amber-800 cpk:dark:bg-amber-500/15 cpk:dark:text-amber-400" : statusString === "complete" ? "cpk:bg-emerald-100 cpk:text-emerald-800 cpk:dark:bg-emerald-500/15 cpk:dark:text-emerald-400" : "cpk:bg-zinc-100 cpk:text-zinc-800 cpk:dark:bg-zinc-700/40 cpk:dark:text-zinc-300"}`,
10678
- children: String(status)
10679
- })]
10680
- }), isExpanded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10681
- className: "cpk:mt-3 cpk:grid cpk:gap-4",
10682
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
10683
- className: "cpk:text-xs cpk:uppercase cpk:tracking-wide cpk:text-zinc-500 cpk:dark:text-zinc-400",
10684
- children: "Arguments"
10685
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
10686
- className: "cpk:mt-2 cpk:max-h-64 cpk:overflow-auto cpk:rounded-md cpk:bg-zinc-50 cpk:dark:bg-zinc-800/60 cpk:p-3 cpk:text-xs cpk:leading-relaxed cpk:text-zinc-800 cpk:dark:text-zinc-200 cpk:whitespace-pre-wrap cpk:break-words",
10687
- children: JSON.stringify(args ?? {}, null, 2)
10688
- })] }), result !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
10689
- className: "cpk:text-xs cpk:uppercase cpk:tracking-wide cpk:text-zinc-500 cpk:dark:text-zinc-400",
10690
- children: "Result"
10691
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
10692
- className: "cpk:mt-2 cpk:max-h-64 cpk:overflow-auto cpk:rounded-md cpk:bg-zinc-50 cpk:dark:bg-zinc-800/60 cpk:p-3 cpk:text-xs cpk:leading-relaxed cpk:text-zinc-800 cpk:dark:text-zinc-200 cpk:whitespace-pre-wrap cpk:break-words",
10693
- children: typeof result === "string" ? result : JSON.stringify(result, null, 2)
10694
- })] })]
10695
- })]
10696
- })
10697
- });
10698
- }
12332
+ group: true,
12333
+ render: (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultToolCallRenderer, { ...adaptRendererProps(props) })
10699
12334
  });
10700
12335
 
10701
12336
  //#endregion
@@ -12974,6 +14609,12 @@ Object.defineProperty(exports, 'CopilotChatToggleButton', {
12974
14609
  return CopilotChatToggleButton;
12975
14610
  }
12976
14611
  });
14612
+ Object.defineProperty(exports, 'CopilotChatToolCallGroup', {
14613
+ enumerable: true,
14614
+ get: function () {
14615
+ return CopilotChatToolCallGroup;
14616
+ }
14617
+ });
12977
14618
  Object.defineProperty(exports, 'CopilotChatToolCallsView', {
12978
14619
  enumerable: true,
12979
14620
  get: function () {
@@ -12992,6 +14633,12 @@ Object.defineProperty(exports, 'CopilotChatView_default', {
12992
14633
  return CopilotChatView_default;
12993
14634
  }
12994
14635
  });
14636
+ Object.defineProperty(exports, 'CopilotChatWorkSummary', {
14637
+ enumerable: true,
14638
+ get: function () {
14639
+ return CopilotChatWorkSummary;
14640
+ }
14641
+ });
12995
14642
  Object.defineProperty(exports, 'CopilotContext', {
12996
14643
  enumerable: true,
12997
14644
  get: function () {
@@ -13394,4 +15041,4 @@ Object.defineProperty(exports, 'useToast', {
13394
15041
  return useToast;
13395
15042
  }
13396
15043
  });
13397
- //# sourceMappingURL=copilotkit-DEaA0Cat.cjs.map
15044
+ //# sourceMappingURL=copilotkit-DKICi3nC.cjs.map