@copilotkit/react-core 1.54.1 → 1.55.0-next.7

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 (183) hide show
  1. package/CHANGELOG.md +117 -116
  2. package/dist/copilotkit-B3Mb1yVE.cjs +7975 -0
  3. package/dist/copilotkit-B3Mb1yVE.cjs.map +1 -0
  4. package/dist/copilotkit-DBzgOMby.d.cts +2182 -0
  5. package/dist/copilotkit-DBzgOMby.d.cts.map +1 -0
  6. package/dist/copilotkit-DNYSFuz5.mjs +7562 -0
  7. package/dist/copilotkit-DNYSFuz5.mjs.map +1 -0
  8. package/dist/copilotkit-Dy5w3qEV.d.mts +2182 -0
  9. package/dist/copilotkit-Dy5w3qEV.d.mts.map +1 -0
  10. package/dist/index.cjs +27 -28
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +3 -3
  13. package/dist/index.d.cts.map +1 -1
  14. package/dist/index.d.mts +3 -3
  15. package/dist/index.d.mts.map +1 -1
  16. package/dist/index.mjs +4 -5
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/index.umd.js +1941 -35
  19. package/dist/index.umd.js.map +1 -1
  20. package/dist/v2/index.cjs +77 -7
  21. package/dist/v2/index.css +1 -2
  22. package/dist/v2/index.d.cts +6 -4
  23. package/dist/v2/index.d.mts +6 -4
  24. package/dist/v2/index.mjs +7 -4
  25. package/dist/v2/index.umd.js +5725 -24
  26. package/dist/v2/index.umd.js.map +1 -1
  27. package/package.json +37 -9
  28. package/scripts/scope-preflight.mjs +101 -0
  29. package/src/components/CopilotListeners.tsx +2 -6
  30. package/src/components/copilot-provider/copilot-messages.tsx +1 -1
  31. package/src/components/copilot-provider/copilotkit-props.tsx +1 -1
  32. package/src/components/copilot-provider/copilotkit.tsx +4 -4
  33. package/src/context/copilot-messages-context.tsx +1 -1
  34. package/src/hooks/__tests__/use-coagent-config.test.ts +2 -2
  35. package/src/hooks/__tests__/use-coagent-state-render.e2e.test.tsx +2 -2
  36. package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +3 -7
  37. package/src/hooks/__tests__/use-frontend-tool-available.test.tsx +1 -1
  38. package/src/hooks/__tests__/use-frontend-tool-remount.e2e.test.tsx +4 -4
  39. package/src/hooks/use-agent-nodename.ts +1 -1
  40. package/src/hooks/use-coagent-state-render-bridge.tsx +1 -4
  41. package/src/hooks/use-coagent.ts +1 -1
  42. package/src/hooks/use-configure-chat-suggestions.tsx +2 -2
  43. package/src/hooks/use-copilot-chat-suggestions.tsx +2 -2
  44. package/src/hooks/use-copilot-chat_internal.ts +2 -2
  45. package/src/hooks/use-copilot-readable.ts +1 -1
  46. package/src/hooks/use-frontend-tool.ts +2 -2
  47. package/src/hooks/use-human-in-the-loop.ts +2 -2
  48. package/src/hooks/use-langgraph-interrupt.ts +2 -5
  49. package/src/hooks/use-lazy-tool-renderer.tsx +1 -1
  50. package/src/hooks/use-render-tool-call.ts +1 -1
  51. package/src/lib/copilot-task.ts +1 -1
  52. package/src/setupTests.ts +18 -14
  53. package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +176 -0
  54. package/src/v2/__tests__/globalSetup.ts +14 -0
  55. package/src/v2/__tests__/setup.ts +93 -0
  56. package/src/v2/__tests__/utils/test-helpers.tsx +470 -0
  57. package/src/v2/a2ui/A2UIMessageRenderer.tsx +206 -0
  58. package/src/v2/components/CopilotKitInspector.tsx +50 -0
  59. package/src/v2/components/MCPAppsActivityRenderer.tsx +785 -0
  60. package/src/v2/components/WildcardToolCallRender.tsx +86 -0
  61. package/src/v2/components/__tests__/license-warning-banner.test.tsx +46 -0
  62. package/src/v2/components/chat/CopilotChat.tsx +431 -0
  63. package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +375 -0
  64. package/src/v2/components/chat/CopilotChatAudioRecorder.tsx +350 -0
  65. package/src/v2/components/chat/CopilotChatInput.tsx +1302 -0
  66. package/src/v2/components/chat/CopilotChatMessageView.tsx +556 -0
  67. package/src/v2/components/chat/CopilotChatReasoningMessage.tsx +252 -0
  68. package/src/v2/components/chat/CopilotChatSuggestionPill.tsx +59 -0
  69. package/src/v2/components/chat/CopilotChatSuggestionView.tsx +133 -0
  70. package/src/v2/components/chat/CopilotChatToggleButton.tsx +171 -0
  71. package/src/v2/components/chat/CopilotChatToolCallsView.tsx +40 -0
  72. package/src/v2/components/chat/CopilotChatUserMessage.tsx +388 -0
  73. package/src/v2/components/chat/CopilotChatView.tsx +598 -0
  74. package/src/v2/components/chat/CopilotModalHeader.tsx +129 -0
  75. package/src/v2/components/chat/CopilotPopup.tsx +81 -0
  76. package/src/v2/components/chat/CopilotPopupView.tsx +317 -0
  77. package/src/v2/components/chat/CopilotSidebar.tsx +76 -0
  78. package/src/v2/components/chat/CopilotSidebarView.tsx +255 -0
  79. package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.tsx +1113 -0
  80. package/src/v2/components/chat/__tests__/CopilotChat.onError.test.tsx +73 -0
  81. package/src/v2/components/chat/__tests__/CopilotChat.slots.e2e.test.tsx +432 -0
  82. package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +150 -0
  83. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.slots.e2e.test.tsx +624 -0
  84. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.tsx +702 -0
  85. package/src/v2/components/chat/__tests__/CopilotChatCssClasses.test.tsx +107 -0
  86. package/src/v2/components/chat/__tests__/CopilotChatInput.slots.e2e.test.tsx +929 -0
  87. package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +986 -0
  88. package/src/v2/components/chat/__tests__/CopilotChatMessageView.slots.e2e.test.tsx +1004 -0
  89. package/src/v2/components/chat/__tests__/CopilotChatMessageView.test.tsx +169 -0
  90. package/src/v2/components/chat/__tests__/CopilotChatSuggestionView.slots.e2e.test.tsx +530 -0
  91. package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.tsx +782 -0
  92. package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +2413 -0
  93. package/src/v2/components/chat/__tests__/CopilotChatUserMessage.slots.e2e.test.tsx +621 -0
  94. package/src/v2/components/chat/__tests__/CopilotChatView.onClick.e2e.test.tsx +853 -0
  95. package/src/v2/components/chat/__tests__/CopilotChatView.slots.e2e.test.tsx +1050 -0
  96. package/src/v2/components/chat/__tests__/CopilotModalHeader.slots.e2e.test.tsx +484 -0
  97. package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.tsx +612 -0
  98. package/src/v2/components/chat/__tests__/CopilotSidebarView.slots.e2e.test.tsx +502 -0
  99. package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +1011 -0
  100. package/src/v2/components/chat/__tests__/setup.ts +1 -0
  101. package/src/v2/components/chat/index.ts +79 -0
  102. package/src/v2/components/index.ts +7 -0
  103. package/src/v2/components/license-warning-banner.tsx +198 -0
  104. package/src/v2/components/ui/button.tsx +123 -0
  105. package/src/v2/components/ui/dropdown-menu.tsx +258 -0
  106. package/src/v2/components/ui/tooltip.tsx +60 -0
  107. package/src/v2/hooks/__tests__/standard-schema-types.test.tsx +152 -0
  108. package/src/v2/hooks/__tests__/standard-schema.test.tsx +282 -0
  109. package/src/v2/hooks/__tests__/use-agent-context-timing.e2e.test.tsx +132 -0
  110. package/src/v2/hooks/__tests__/use-agent-context.test.tsx +401 -0
  111. package/src/v2/hooks/__tests__/use-agent-error-state.test.tsx +44 -0
  112. package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +205 -0
  113. package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +148 -0
  114. package/src/v2/hooks/__tests__/use-component.test.tsx +123 -0
  115. package/src/v2/hooks/__tests__/use-configure-suggestions.e2e.test.tsx +696 -0
  116. package/src/v2/hooks/__tests__/use-default-render-tool.test.tsx +153 -0
  117. package/src/v2/hooks/__tests__/use-frontend-tool-available.test.tsx +167 -0
  118. package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +2129 -0
  119. package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.tsx +1261 -0
  120. package/src/v2/hooks/__tests__/use-interrupt.test.tsx +397 -0
  121. package/src/v2/hooks/__tests__/use-katex-styles.test.tsx +56 -0
  122. package/src/v2/hooks/__tests__/use-keyboard-height.test.tsx +192 -0
  123. package/src/v2/hooks/__tests__/use-render-tool.test.tsx +259 -0
  124. package/src/v2/hooks/__tests__/use-suggestions.e2e.test.tsx +524 -0
  125. package/src/v2/hooks/__tests__/use-threads.test.tsx +433 -0
  126. package/src/v2/hooks/__tests__/zod-regression.test.tsx +311 -0
  127. package/src/v2/hooks/index.ts +18 -0
  128. package/src/v2/hooks/use-agent-context.tsx +45 -0
  129. package/src/v2/hooks/use-agent.tsx +155 -0
  130. package/src/v2/hooks/use-component.tsx +89 -0
  131. package/src/v2/hooks/use-configure-suggestions.tsx +187 -0
  132. package/src/v2/hooks/use-default-render-tool.tsx +254 -0
  133. package/src/v2/hooks/use-frontend-tool.tsx +43 -0
  134. package/src/v2/hooks/use-human-in-the-loop.tsx +81 -0
  135. package/src/v2/hooks/use-interrupt.tsx +305 -0
  136. package/src/v2/hooks/use-keyboard-height.tsx +67 -0
  137. package/src/v2/hooks/use-render-activity-message.tsx +73 -0
  138. package/src/v2/hooks/use-render-custom-messages.tsx +93 -0
  139. package/src/v2/hooks/use-render-tool-call.tsx +175 -0
  140. package/src/v2/hooks/use-render-tool.tsx +181 -0
  141. package/src/v2/hooks/use-suggestions.tsx +91 -0
  142. package/src/v2/hooks/use-threads.tsx +256 -0
  143. package/src/v2/hooks/useKatexStyles.ts +27 -0
  144. package/src/v2/index.css +1 -1
  145. package/src/v2/index.ts +18 -2
  146. package/src/v2/lib/__tests__/completePartialMarkdown.test.ts +495 -0
  147. package/src/v2/lib/__tests__/renderSlot.test.tsx +588 -0
  148. package/src/v2/lib/react-core.ts +156 -0
  149. package/src/v2/lib/slots.tsx +143 -0
  150. package/src/v2/lib/transcription-client.ts +184 -0
  151. package/src/v2/lib/utils.ts +8 -0
  152. package/src/v2/providers/CopilotChatConfigurationProvider.tsx +162 -0
  153. package/src/v2/providers/CopilotKitProvider.tsx +600 -0
  154. package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.tsx +546 -0
  155. package/src/v2/providers/__tests__/CopilotKitProvider.license.test.tsx +101 -0
  156. package/src/v2/providers/__tests__/CopilotKitProvider.onError.test.tsx +69 -0
  157. package/src/v2/providers/__tests__/CopilotKitProvider.renderCustomMessages.e2e.test.tsx +881 -0
  158. package/src/v2/providers/__tests__/CopilotKitProvider.stability.test.tsx +740 -0
  159. package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +642 -0
  160. package/src/v2/providers/__tests__/CopilotKitProvider.wildcard.test.tsx +294 -0
  161. package/src/v2/providers/index.ts +14 -0
  162. package/src/v2/styles/globals.css +230 -0
  163. package/src/v2/types/__tests__/defineToolCallRenderer.test.tsx +525 -0
  164. package/src/v2/types/defineToolCallRenderer.ts +65 -0
  165. package/src/v2/types/frontend-tool.ts +8 -0
  166. package/src/v2/types/human-in-the-loop.ts +33 -0
  167. package/src/v2/types/index.ts +7 -0
  168. package/src/v2/types/interrupt.ts +15 -0
  169. package/src/v2/types/react-activity-message-renderer.ts +27 -0
  170. package/src/v2/types/react-custom-message-renderer.ts +17 -0
  171. package/src/v2/types/react-tool-call-renderer.ts +32 -0
  172. package/tsdown.config.ts +34 -10
  173. package/vitest.config.mjs +4 -3
  174. package/LICENSE +0 -21
  175. package/dist/copilotkit-BRPQ2sqS.d.cts +0 -670
  176. package/dist/copilotkit-BRPQ2sqS.d.cts.map +0 -1
  177. package/dist/copilotkit-C94ayZbs.cjs +0 -2161
  178. package/dist/copilotkit-C94ayZbs.cjs.map +0 -1
  179. package/dist/copilotkit-CwZMFmSK.d.mts +0 -670
  180. package/dist/copilotkit-CwZMFmSK.d.mts.map +0 -1
  181. package/dist/copilotkit-Yh_Ld_FX.mjs +0 -2031
  182. package/dist/copilotkit-Yh_Ld_FX.mjs.map +0 -1
  183. package/dist/v2/index.css.map +0 -1
@@ -0,0 +1,86 @@
1
+ import { defineToolCallRenderer } from "../types/defineToolCallRenderer";
2
+ import { useState } from "react";
3
+
4
+ export const WildcardToolCallRender = defineToolCallRenderer({
5
+ name: "*",
6
+ render: ({ args, result, name, status }) => {
7
+ const [isExpanded, setIsExpanded] = useState(false);
8
+
9
+ const statusString = String(status) as
10
+ | "inProgress"
11
+ | "executing"
12
+ | "complete";
13
+ const isActive =
14
+ statusString === "inProgress" || statusString === "executing";
15
+ const isComplete = statusString === "complete";
16
+ const statusStyles = isActive
17
+ ? "cpk:bg-amber-100 cpk:text-amber-800 cpk:dark:bg-amber-500/15 cpk:dark:text-amber-400"
18
+ : isComplete
19
+ ? "cpk:bg-emerald-100 cpk:text-emerald-800 cpk:dark:bg-emerald-500/15 cpk:dark:text-emerald-400"
20
+ : "cpk:bg-zinc-100 cpk:text-zinc-800 cpk:dark:bg-zinc-700/40 cpk:dark:text-zinc-300";
21
+
22
+ return (
23
+ <div className="cpk:mt-2 cpk:pb-2">
24
+ <div 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">
25
+ <div
26
+ className="cpk:flex cpk:items-center cpk:justify-between cpk:gap-3 cpk:cursor-pointer"
27
+ onClick={() => setIsExpanded(!isExpanded)}
28
+ >
29
+ <div className="cpk:flex cpk:items-center cpk:gap-2 cpk:min-w-0">
30
+ <svg
31
+ className={`cpk:h-4 cpk:w-4 cpk:text-zinc-500 cpk:dark:text-zinc-400 cpk:transition-transform ${
32
+ isExpanded ? "cpk:rotate-90" : ""
33
+ }`}
34
+ fill="none"
35
+ viewBox="0 0 24 24"
36
+ strokeWidth={2}
37
+ stroke="currentColor"
38
+ >
39
+ <path
40
+ strokeLinecap="round"
41
+ strokeLinejoin="round"
42
+ d="M8.25 4.5l7.5 7.5-7.5 7.5"
43
+ />
44
+ </svg>
45
+ <span className="cpk:inline-block cpk:h-2 cpk:w-2 cpk:rounded-full cpk:bg-blue-500" />
46
+ <span className="cpk:truncate cpk:text-sm cpk:font-medium cpk:text-zinc-900 cpk:dark:text-zinc-100">
47
+ {name}
48
+ </span>
49
+ </div>
50
+ <span
51
+ className={`cpk:inline-flex cpk:items-center cpk:rounded-full cpk:px-2 cpk:py-1 cpk:text-xs cpk:font-medium ${statusStyles}`}
52
+ >
53
+ {String(status)}
54
+ </span>
55
+ </div>
56
+
57
+ {isExpanded && (
58
+ <div className="cpk:mt-3 cpk:grid cpk:gap-4">
59
+ <div>
60
+ <div className="cpk:text-xs cpk:uppercase cpk:tracking-wide cpk:text-zinc-500 cpk:dark:text-zinc-400">
61
+ Arguments
62
+ </div>
63
+ <pre 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">
64
+ {JSON.stringify(args ?? {}, null, 2)}
65
+ </pre>
66
+ </div>
67
+
68
+ {result !== undefined && (
69
+ <div>
70
+ <div className="cpk:text-xs cpk:uppercase cpk:tracking-wide cpk:text-zinc-500 cpk:dark:text-zinc-400">
71
+ Result
72
+ </div>
73
+ <pre 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">
74
+ {typeof result === "string"
75
+ ? result
76
+ : JSON.stringify(result, null, 2)}
77
+ </pre>
78
+ </div>
79
+ )}
80
+ </div>
81
+ )}
82
+ </div>
83
+ </div>
84
+ );
85
+ },
86
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import React from "react";
4
+ import {
5
+ LicenseWarningBanner,
6
+ InlineFeatureWarning,
7
+ } from "../license-warning-banner";
8
+
9
+ describe("LicenseWarningBanner", () => {
10
+ it("renders 'Powered by CopilotKit' for no_license type", () => {
11
+ render(<LicenseWarningBanner type="no_license" />);
12
+ expect(screen.getByText("Powered by CopilotKit")).toBeTruthy();
13
+ });
14
+
15
+ it("renders feature name for feature_unlicensed type", () => {
16
+ render(
17
+ <LicenseWarningBanner type="feature_unlicensed" featureName="Chat" />,
18
+ );
19
+ expect(
20
+ screen.getByText(/Chat.*requires a CopilotKit license/),
21
+ ).toBeTruthy();
22
+ });
23
+
24
+ it("renders expiry warning with days remaining", () => {
25
+ render(<LicenseWarningBanner type="expiring" graceRemaining={3} />);
26
+ expect(screen.getByText(/expires in 3 days/)).toBeTruthy();
27
+ });
28
+
29
+ it("renders critical expired banner", () => {
30
+ render(<LicenseWarningBanner type="expired" expiryDate="2026-03-01" />);
31
+ expect(screen.getByText(/expired/i)).toBeTruthy();
32
+ });
33
+
34
+ it("renders invalid license banner", () => {
35
+ render(<LicenseWarningBanner type="invalid" />);
36
+ expect(screen.getByText(/Invalid CopilotKit license/)).toBeTruthy();
37
+ });
38
+ });
39
+
40
+ describe("InlineFeatureWarning", () => {
41
+ it("renders with feature name and pricing link", () => {
42
+ render(<InlineFeatureWarning featureName="Agents" />);
43
+ expect(screen.getByText(/requires a CopilotKit license/)).toBeTruthy();
44
+ expect(screen.getByRole("link")).toBeTruthy();
45
+ });
46
+ });
@@ -0,0 +1,431 @@
1
+ import { useAgent } from "../../hooks/use-agent";
2
+ import { useSuggestions } from "../../hooks/use-suggestions";
3
+ import { CopilotChatView, CopilotChatViewProps } from "./CopilotChatView";
4
+ import { CopilotChatInputMode } from "./CopilotChatInput";
5
+ import {
6
+ CopilotChatConfigurationProvider,
7
+ CopilotChatLabels,
8
+ useCopilotChatConfiguration,
9
+ } from "../../providers/CopilotChatConfigurationProvider";
10
+ import {
11
+ DEFAULT_AGENT_ID,
12
+ randomUUID,
13
+ TranscriptionErrorCode,
14
+ } from "@copilotkit/shared";
15
+ import { Suggestion, CopilotKitCoreErrorCode } from "@copilotkit/core";
16
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
17
+ import { merge } from "ts-deepmerge";
18
+ import {
19
+ useCopilotKit,
20
+ useLicenseContext,
21
+ } from "../../providers/CopilotKitProvider";
22
+ import { InlineFeatureWarning } from "../../components/license-warning-banner";
23
+ import { AbstractAgent, HttpAgent } from "@ag-ui/client";
24
+ import { renderSlot, SlotValue } from "../../lib/slots";
25
+ import {
26
+ transcribeAudio,
27
+ TranscriptionError,
28
+ } from "../../lib/transcription-client";
29
+
30
+ export type CopilotChatProps = Omit<
31
+ CopilotChatViewProps,
32
+ | "messages"
33
+ | "isRunning"
34
+ | "suggestions"
35
+ | "suggestionLoadingIndexes"
36
+ | "onSelectSuggestion"
37
+ > & {
38
+ agentId?: string;
39
+ threadId?: string;
40
+ labels?: Partial<CopilotChatLabels>;
41
+ chatView?: SlotValue<typeof CopilotChatView>;
42
+ isModalDefaultOpen?: boolean;
43
+ /**
44
+ * Error handler scoped to this chat's agent. Fires in addition to the
45
+ * provider-level onError (does not suppress it). Receives only errors
46
+ * whose context.agentId matches this chat's agent.
47
+ */
48
+ onError?: (event: {
49
+ error: Error;
50
+ code: CopilotKitCoreErrorCode;
51
+ context: Record<string, any>;
52
+ }) => void | Promise<void>;
53
+ };
54
+ export function CopilotChat({
55
+ agentId,
56
+ threadId,
57
+ labels,
58
+ chatView,
59
+ isModalDefaultOpen,
60
+ onError,
61
+ ...props
62
+ }: CopilotChatProps) {
63
+ // Check for existing configuration provider
64
+ const existingConfig = useCopilotChatConfiguration();
65
+
66
+ // Apply priority: props > existing config > defaults
67
+ const resolvedAgentId =
68
+ agentId ?? existingConfig?.agentId ?? DEFAULT_AGENT_ID;
69
+ const resolvedThreadId = useMemo(
70
+ () => threadId ?? existingConfig?.threadId ?? randomUUID(),
71
+ [threadId, existingConfig?.threadId],
72
+ );
73
+
74
+ const { agent } = useAgent({ agentId: resolvedAgentId });
75
+ const { copilotkit } = useCopilotKit();
76
+ const { suggestions: autoSuggestions } = useSuggestions({
77
+ agentId: resolvedAgentId,
78
+ });
79
+
80
+ const { checkFeature } = useLicenseContext();
81
+ const isChatLicensed = checkFeature("chat");
82
+
83
+ useEffect(() => {
84
+ if (!isChatLicensed) {
85
+ console.warn(
86
+ '[CopilotKit] Warning: "chat" feature is not licensed. Visit copilotkit.ai/pricing',
87
+ );
88
+ }
89
+ }, [isChatLicensed]);
90
+
91
+ // onError subscription — forward core errors scoped to this chat's agent
92
+ const onErrorRef = useRef(onError);
93
+ useEffect(() => {
94
+ onErrorRef.current = onError;
95
+ }, [onError]);
96
+
97
+ useEffect(() => {
98
+ if (!onErrorRef.current) return;
99
+
100
+ const subscription = copilotkit.subscribe({
101
+ onError: (event) => {
102
+ // Only forward errors that match this chat's agent
103
+ if (
104
+ event.context?.agentId === resolvedAgentId ||
105
+ !event.context?.agentId
106
+ ) {
107
+ onErrorRef.current?.({
108
+ error: event.error,
109
+ code: event.code,
110
+ context: event.context,
111
+ });
112
+ }
113
+ },
114
+ });
115
+
116
+ return () => {
117
+ subscription.unsubscribe();
118
+ };
119
+ }, [copilotkit, resolvedAgentId]);
120
+
121
+ // Transcription state
122
+ const [transcribeMode, setTranscribeMode] =
123
+ useState<CopilotChatInputMode>("input");
124
+ const [inputValue, setInputValue] = useState("");
125
+ const [transcriptionError, setTranscriptionError] = useState<string | null>(
126
+ null,
127
+ );
128
+ const [isTranscribing, setIsTranscribing] = useState(false);
129
+
130
+ // Check if transcription is enabled
131
+ const isTranscriptionEnabled = copilotkit.audioFileTranscriptionEnabled;
132
+
133
+ // Check if browser supports MediaRecorder
134
+ const isMediaRecorderSupported =
135
+ typeof window !== "undefined" && typeof MediaRecorder !== "undefined";
136
+
137
+ const {
138
+ messageView: providedMessageView,
139
+ suggestionView: providedSuggestionView,
140
+ onStop: providedStopHandler,
141
+ ...restProps
142
+ } = props;
143
+
144
+ useEffect(() => {
145
+ let detached = false;
146
+
147
+ // Create a fresh AbortController so we can cancel the HTTP request on cleanup.
148
+ // HttpAgent (parent of ProxiedCopilotRuntimeAgent) uses this.abortController.signal
149
+ // in its fetch config. Unlike runAgent(), connectAgent() does NOT create a new
150
+ // AbortController automatically, so we must set one before connecting.
151
+ const connectAbortController = new AbortController();
152
+ if (agent instanceof HttpAgent) {
153
+ agent.abortController = connectAbortController;
154
+ }
155
+
156
+ const connect = async (agent: AbstractAgent) => {
157
+ try {
158
+ await copilotkit.connectAgent({ agent });
159
+ } catch (error) {
160
+ // Ignore errors from aborted connections (e.g., React StrictMode cleanup)
161
+ if (detached) return;
162
+ // connectAgent already emits via the subscriber system, but catch
163
+ // here to prevent unhandled rejections from unexpected errors.
164
+ console.error("CopilotChat: connectAgent failed", error);
165
+ }
166
+ };
167
+ agent.threadId = resolvedThreadId;
168
+ connect(agent);
169
+ return () => {
170
+ // Abort the HTTP request and detach the active run.
171
+ // This is critical for React StrictMode which unmounts+remounts in dev,
172
+ // preventing duplicate /connect requests from reaching the server.
173
+ detached = true;
174
+ connectAbortController.abort();
175
+ // The .catch() is required to prevent a false-positive "Uncaught (in promise)
176
+ // AbortError" in browser devtools. detachActiveRun() itself does not reject,
177
+ // but without an attached handler V8 flags the promise chain as unhandled
178
+ // when the abort signal propagates through connected promises internally.
179
+ void agent.detachActiveRun().catch(() => {});
180
+ };
181
+ // copilotkit is intentionally excluded — it is a stable ref that never changes.
182
+ // eslint-disable-next-line react-hooks/exhaustive-deps
183
+ }, [resolvedThreadId, agent, resolvedAgentId]);
184
+
185
+ const onSubmitInput = useCallback(
186
+ async (value: string) => {
187
+ agent.addMessage({
188
+ id: randomUUID(),
189
+ role: "user",
190
+ content: value,
191
+ });
192
+ // Clear input after submitting
193
+ setInputValue("");
194
+ try {
195
+ await copilotkit.runAgent({ agent });
196
+ } catch (error) {
197
+ console.error("CopilotChat: runAgent failed", error);
198
+ }
199
+ },
200
+ // copilotkit is intentionally excluded — it is a stable ref that never changes.
201
+ // eslint-disable-next-line react-hooks/exhaustive-deps
202
+ [agent],
203
+ );
204
+
205
+ const handleSelectSuggestion = useCallback(
206
+ async (suggestion: Suggestion) => {
207
+ agent.addMessage({
208
+ id: randomUUID(),
209
+ role: "user",
210
+ content: suggestion.message,
211
+ });
212
+
213
+ try {
214
+ await copilotkit.runAgent({ agent });
215
+ } catch (error) {
216
+ console.error(
217
+ "CopilotChat: runAgent failed after selecting suggestion",
218
+ error,
219
+ );
220
+ }
221
+ },
222
+ // eslint-disable-next-line react-hooks/exhaustive-deps
223
+ [agent],
224
+ );
225
+
226
+ const stopCurrentRun = useCallback(() => {
227
+ try {
228
+ copilotkit.stopAgent({ agent });
229
+ } catch (error) {
230
+ console.error("CopilotChat: stopAgent failed", error);
231
+ try {
232
+ agent.abortRun();
233
+ } catch (abortError) {
234
+ console.error("CopilotChat: abortRun fallback failed", abortError);
235
+ }
236
+ }
237
+ // eslint-disable-next-line react-hooks/exhaustive-deps
238
+ }, [agent]);
239
+
240
+ // Transcription handlers
241
+ const handleStartTranscribe = useCallback(() => {
242
+ setTranscriptionError(null);
243
+ setTranscribeMode("transcribe");
244
+ }, []);
245
+
246
+ const handleCancelTranscribe = useCallback(() => {
247
+ setTranscriptionError(null);
248
+ setTranscribeMode("input");
249
+ }, []);
250
+
251
+ const handleFinishTranscribe = useCallback(() => {
252
+ setTranscribeMode("input");
253
+ }, []);
254
+
255
+ // Handle audio blob from CopilotChatInput and transcribe it
256
+ const handleFinishTranscribeWithAudio = useCallback(
257
+ async (audioBlob: Blob) => {
258
+ setIsTranscribing(true);
259
+ try {
260
+ setTranscriptionError(null);
261
+
262
+ // Send to transcription endpoint
263
+ const result = await transcribeAudio(copilotkit, audioBlob);
264
+
265
+ // Insert transcribed text into input
266
+ setInputValue((prev) => {
267
+ const trimmedPrev = prev.trim();
268
+ if (trimmedPrev) {
269
+ return `${trimmedPrev} ${result.text}`;
270
+ }
271
+ return result.text;
272
+ });
273
+ } catch (error) {
274
+ console.error("CopilotChat: Transcription failed", error);
275
+
276
+ // Show contextual error message based on error type
277
+ if (error instanceof TranscriptionError) {
278
+ const { code, retryable, message } = error.info;
279
+ switch (code) {
280
+ case TranscriptionErrorCode.RATE_LIMITED:
281
+ setTranscriptionError("Too many requests. Please wait a moment.");
282
+ break;
283
+ case TranscriptionErrorCode.AUTH_FAILED:
284
+ setTranscriptionError(
285
+ "Authentication error. Please check your configuration.",
286
+ );
287
+ break;
288
+ case TranscriptionErrorCode.AUDIO_TOO_LONG:
289
+ setTranscriptionError(
290
+ "Recording is too long. Please try a shorter recording.",
291
+ );
292
+ break;
293
+ case TranscriptionErrorCode.AUDIO_TOO_SHORT:
294
+ setTranscriptionError(
295
+ "Recording is too short. Please try again.",
296
+ );
297
+ break;
298
+ case TranscriptionErrorCode.INVALID_AUDIO_FORMAT:
299
+ setTranscriptionError("Audio format not supported.");
300
+ break;
301
+ case TranscriptionErrorCode.SERVICE_NOT_CONFIGURED:
302
+ setTranscriptionError("Transcription service is not available.");
303
+ break;
304
+ case TranscriptionErrorCode.NETWORK_ERROR:
305
+ setTranscriptionError(
306
+ "Network error. Please check your connection.",
307
+ );
308
+ break;
309
+ default:
310
+ // For retryable errors, show more helpful message
311
+ setTranscriptionError(
312
+ retryable ? "Transcription failed. Please try again." : message,
313
+ );
314
+ }
315
+ } else {
316
+ // Fallback for unexpected errors
317
+ setTranscriptionError("Transcription failed. Please try again.");
318
+ }
319
+ } finally {
320
+ setIsTranscribing(false);
321
+ }
322
+ },
323
+ // eslint-disable-next-line react-hooks/exhaustive-deps
324
+ [],
325
+ );
326
+
327
+ // Clear transcription error after a delay
328
+ useEffect(() => {
329
+ if (transcriptionError) {
330
+ const timer = setTimeout(() => {
331
+ setTranscriptionError(null);
332
+ }, 5000);
333
+ return () => clearTimeout(timer);
334
+ }
335
+ }, [transcriptionError]);
336
+
337
+ const mergedProps = merge(
338
+ {
339
+ isRunning: agent.isRunning,
340
+ suggestions: autoSuggestions,
341
+ onSelectSuggestion: handleSelectSuggestion,
342
+ suggestionView: providedSuggestionView,
343
+ },
344
+ {
345
+ ...restProps,
346
+ ...(typeof providedMessageView === "string"
347
+ ? { messageView: { className: providedMessageView } }
348
+ : providedMessageView !== undefined
349
+ ? { messageView: providedMessageView }
350
+ : {}),
351
+ },
352
+ );
353
+
354
+ const hasMessages = agent.messages.length > 0;
355
+ const shouldAllowStop = agent.isRunning && hasMessages;
356
+ const effectiveStopHandler = shouldAllowStop
357
+ ? (providedStopHandler ?? stopCurrentRun)
358
+ : providedStopHandler;
359
+
360
+ // Determine if transcription feature should be available
361
+ const showTranscription = isTranscriptionEnabled && isMediaRecorderSupported;
362
+
363
+ // Determine mode: transcribing takes priority, then transcribe mode, then default to input
364
+ const effectiveMode: CopilotChatInputMode = isTranscribing
365
+ ? "processing"
366
+ : transcribeMode;
367
+
368
+ // Memoize messages array - only create new reference when content actually changes
369
+ // (agent.messages is mutated in place, so we need a new reference for React to detect changes)
370
+
371
+ const messages = useMemo(
372
+ () => [...agent.messages],
373
+ [JSON.stringify(agent.messages)],
374
+ );
375
+
376
+ const finalProps = merge(mergedProps, {
377
+ messages,
378
+ // Input behavior props
379
+ onSubmitMessage: onSubmitInput,
380
+ onStop: effectiveStopHandler,
381
+ inputMode: effectiveMode,
382
+ inputValue,
383
+ onInputChange: setInputValue,
384
+ // Only provide transcription handlers if feature is available
385
+ onStartTranscribe: showTranscription ? handleStartTranscribe : undefined,
386
+ onCancelTranscribe: showTranscription ? handleCancelTranscribe : undefined,
387
+ onFinishTranscribe: showTranscription ? handleFinishTranscribe : undefined,
388
+ onFinishTranscribeWithAudio: showTranscription
389
+ ? handleFinishTranscribeWithAudio
390
+ : undefined,
391
+ }) as CopilotChatViewProps;
392
+
393
+ // Always create a provider with merged values
394
+ // This ensures priority: props > existing config > defaults
395
+ const RenderedChatView = renderSlot(chatView, CopilotChatView, finalProps);
396
+
397
+ return (
398
+ <CopilotChatConfigurationProvider
399
+ agentId={resolvedAgentId}
400
+ threadId={resolvedThreadId}
401
+ labels={labels}
402
+ isModalDefaultOpen={isModalDefaultOpen}
403
+ >
404
+ {!isChatLicensed && <InlineFeatureWarning featureName="Chat" />}
405
+ {transcriptionError && (
406
+ <div
407
+ style={{
408
+ position: "absolute",
409
+ bottom: "100px",
410
+ left: "50%",
411
+ transform: "translateX(-50%)",
412
+ backgroundColor: "#ef4444",
413
+ color: "white",
414
+ padding: "8px 16px",
415
+ borderRadius: "8px",
416
+ fontSize: "14px",
417
+ zIndex: 50,
418
+ }}
419
+ >
420
+ {transcriptionError}
421
+ </div>
422
+ )}
423
+ {RenderedChatView}
424
+ </CopilotChatConfigurationProvider>
425
+ );
426
+ }
427
+
428
+ // eslint-disable-next-line @typescript-eslint/no-namespace
429
+ export namespace CopilotChat {
430
+ export const View = CopilotChatView;
431
+ }