@copilotkit/react-core 0.0.0-main-20260220223427 → 0.0.0-main-20260403195251

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