@copilotkit/react-core 1.54.1-next.6 → 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 +121 -106
  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 +35 -7
  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,187 @@
1
+ import { useCallback, useEffect, useMemo, useRef } from "react";
2
+ import { useCopilotKit } from "../providers/CopilotKitProvider";
3
+ import { useCopilotChatConfiguration } from "../providers/CopilotChatConfigurationProvider";
4
+ import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
5
+ import {
6
+ DynamicSuggestionsConfig,
7
+ StaticSuggestionsConfig,
8
+ SuggestionsConfig,
9
+ Suggestion,
10
+ } from "@copilotkit/core";
11
+
12
+ type StaticSuggestionInput = Omit<Suggestion, "isLoading"> &
13
+ Partial<Pick<Suggestion, "isLoading">>;
14
+
15
+ type StaticSuggestionsConfigInput = Omit<
16
+ StaticSuggestionsConfig,
17
+ "suggestions"
18
+ > & {
19
+ suggestions: StaticSuggestionInput[];
20
+ };
21
+
22
+ type SuggestionsConfigInput =
23
+ | DynamicSuggestionsConfig
24
+ | StaticSuggestionsConfigInput;
25
+
26
+ export function useConfigureSuggestions(
27
+ config: SuggestionsConfigInput | null | undefined,
28
+ deps?: ReadonlyArray<unknown>,
29
+ ): void {
30
+ const { copilotkit } = useCopilotKit();
31
+ const chatConfig = useCopilotChatConfiguration();
32
+ const extraDeps = deps ?? [];
33
+
34
+ const resolvedConsumerAgentId = useMemo(
35
+ () => chatConfig?.agentId ?? DEFAULT_AGENT_ID,
36
+ [chatConfig?.agentId],
37
+ );
38
+
39
+ const rawConsumerAgentId = useMemo(
40
+ () =>
41
+ config ? (config as SuggestionsConfigInput).consumerAgentId : undefined,
42
+ [config],
43
+ );
44
+
45
+ const normalizationCacheRef = useRef<{
46
+ serialized: string | null;
47
+ config: SuggestionsConfig | null;
48
+ }>({
49
+ serialized: null,
50
+ config: null,
51
+ });
52
+
53
+ const { normalizedConfig, serializedConfig } = useMemo(() => {
54
+ if (!config) {
55
+ normalizationCacheRef.current = { serialized: null, config: null };
56
+ return { normalizedConfig: null, serializedConfig: null };
57
+ }
58
+
59
+ if (config.available === "disabled") {
60
+ normalizationCacheRef.current = { serialized: null, config: null };
61
+ return { normalizedConfig: null, serializedConfig: null };
62
+ }
63
+
64
+ let built: SuggestionsConfig;
65
+ if (isDynamicConfig(config)) {
66
+ built = {
67
+ ...config,
68
+ } satisfies DynamicSuggestionsConfig;
69
+ } else {
70
+ const normalizedSuggestions = normalizeStaticSuggestions(
71
+ config.suggestions,
72
+ );
73
+ const baseConfig: StaticSuggestionsConfig = {
74
+ ...config,
75
+ suggestions: normalizedSuggestions,
76
+ };
77
+ built = baseConfig;
78
+ }
79
+
80
+ const serialized = JSON.stringify(built);
81
+ const cache = normalizationCacheRef.current;
82
+ if (cache.serialized === serialized && cache.config) {
83
+ return { normalizedConfig: cache.config, serializedConfig: serialized };
84
+ }
85
+
86
+ normalizationCacheRef.current = { serialized, config: built };
87
+ return { normalizedConfig: built, serializedConfig: serialized };
88
+ }, [config, resolvedConsumerAgentId, ...extraDeps]);
89
+ const latestConfigRef = useRef<SuggestionsConfig | null>(null);
90
+ latestConfigRef.current = normalizedConfig;
91
+ const previousSerializedConfigRef = useRef<string | null>(null);
92
+
93
+ const targetAgentId = useMemo(() => {
94
+ if (!normalizedConfig) {
95
+ return resolvedConsumerAgentId;
96
+ }
97
+ const consumer = (
98
+ normalizedConfig as StaticSuggestionsConfig | DynamicSuggestionsConfig
99
+ ).consumerAgentId;
100
+ if (!consumer || consumer === "*") {
101
+ return resolvedConsumerAgentId;
102
+ }
103
+ return consumer;
104
+ }, [normalizedConfig, resolvedConsumerAgentId]);
105
+
106
+ const isGlobalConfig =
107
+ rawConsumerAgentId === undefined || rawConsumerAgentId === "*";
108
+
109
+ const requestReload = useCallback(() => {
110
+ if (!normalizedConfig) {
111
+ return;
112
+ }
113
+
114
+ if (isGlobalConfig) {
115
+ const agents = Object.values(copilotkit.agents ?? {});
116
+ for (const entry of agents) {
117
+ const agentId = entry.agentId;
118
+ if (!agentId) {
119
+ continue;
120
+ }
121
+ if (!entry.isRunning) {
122
+ copilotkit.reloadSuggestions(agentId);
123
+ }
124
+ }
125
+ return;
126
+ }
127
+
128
+ if (!targetAgentId) {
129
+ return;
130
+ }
131
+
132
+ copilotkit.reloadSuggestions(targetAgentId);
133
+ }, [copilotkit, isGlobalConfig, normalizedConfig, targetAgentId]);
134
+
135
+ useEffect(() => {
136
+ if (!serializedConfig || !latestConfigRef.current) {
137
+ return;
138
+ }
139
+
140
+ const id = copilotkit.addSuggestionsConfig(latestConfigRef.current);
141
+
142
+ requestReload();
143
+
144
+ return () => {
145
+ copilotkit.removeSuggestionsConfig(id);
146
+ };
147
+ }, [copilotkit, serializedConfig, requestReload]);
148
+
149
+ useEffect(() => {
150
+ if (!normalizedConfig) {
151
+ previousSerializedConfigRef.current = null;
152
+ return;
153
+ }
154
+ if (
155
+ serializedConfig &&
156
+ previousSerializedConfigRef.current === serializedConfig
157
+ ) {
158
+ return;
159
+ }
160
+ if (serializedConfig) {
161
+ previousSerializedConfigRef.current = serializedConfig;
162
+ }
163
+ requestReload();
164
+ }, [normalizedConfig, requestReload, serializedConfig]);
165
+
166
+ useEffect(() => {
167
+ if (!normalizedConfig || extraDeps.length === 0) {
168
+ return;
169
+ }
170
+ requestReload();
171
+ }, [extraDeps.length, normalizedConfig, requestReload, ...extraDeps]);
172
+ }
173
+
174
+ function isDynamicConfig(
175
+ config: SuggestionsConfigInput,
176
+ ): config is DynamicSuggestionsConfig {
177
+ return "instructions" in config;
178
+ }
179
+
180
+ function normalizeStaticSuggestions(
181
+ suggestions: StaticSuggestionInput[],
182
+ ): Suggestion[] {
183
+ return suggestions.map((suggestion) => ({
184
+ ...suggestion,
185
+ isLoading: suggestion.isLoading ?? false,
186
+ }));
187
+ }
@@ -0,0 +1,254 @@
1
+ import React, { useState } from "react";
2
+ import { useRenderTool } from "./use-render-tool";
3
+
4
+ type DefaultRenderProps = {
5
+ /** The name of the tool being called. */
6
+ name: string;
7
+ /** The parsed parameters passed to the tool call. */
8
+ parameters: unknown;
9
+ /** Current execution status of the tool call. */
10
+ status: "inProgress" | "executing" | "complete";
11
+ /** The tool call result string, available only when `status` is `"complete"`. */
12
+ result: string | undefined;
13
+ };
14
+
15
+ /**
16
+ * Registers a wildcard (`"*"`) tool-call renderer via `useRenderTool`.
17
+ *
18
+ * - Call with no config to use CopilotKit's built-in default tool-call card.
19
+ * - Pass `config.render` to replace the default UI with your own fallback renderer.
20
+ *
21
+ * This is useful when you want a generic renderer for tools that do not have a
22
+ * dedicated `useRenderTool({ name: "..." })` registration.
23
+ *
24
+ * @param config - Optional custom wildcard render function.
25
+ * @param deps - Optional dependencies to refresh registration.
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * useDefaultRenderTool();
30
+ * ```
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * useDefaultRenderTool({
35
+ * render: ({ name, status }) => <div>{name}: {status}</div>,
36
+ * });
37
+ * ```
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * useDefaultRenderTool(
42
+ * {
43
+ * render: ({ name, result }) => (
44
+ * <ToolEventRow title={name} payload={result} compact={compactMode} />
45
+ * ),
46
+ * },
47
+ * [compactMode],
48
+ * );
49
+ * ```
50
+ */
51
+ export function useDefaultRenderTool(
52
+ config?: {
53
+ render?: (props: DefaultRenderProps) => React.ReactElement;
54
+ },
55
+ deps?: ReadonlyArray<unknown>,
56
+ ): void {
57
+ useRenderTool(
58
+ {
59
+ name: "*",
60
+ render: config?.render ?? DefaultToolCallRenderer,
61
+ },
62
+ deps,
63
+ );
64
+ }
65
+
66
+ function DefaultToolCallRenderer({
67
+ name,
68
+ parameters,
69
+ status,
70
+ result,
71
+ }: DefaultRenderProps): React.ReactElement {
72
+ const [isExpanded, setIsExpanded] = useState(false);
73
+
74
+ const statusString = String(status) as
75
+ | "inProgress"
76
+ | "executing"
77
+ | "complete";
78
+ const isActive =
79
+ statusString === "inProgress" || statusString === "executing";
80
+ const isComplete = statusString === "complete";
81
+
82
+ const statusLabel = isActive ? "Running" : isComplete ? "Done" : status;
83
+ const dotColor = isActive ? "#f59e0b" : isComplete ? "#10b981" : "#a1a1aa";
84
+ const badgeBg = isActive ? "#fef3c7" : isComplete ? "#d1fae5" : "#f4f4f5";
85
+ const badgeColor = isActive ? "#92400e" : isComplete ? "#065f46" : "#3f3f46";
86
+
87
+ return (
88
+ <div
89
+ style={{
90
+ marginTop: "8px",
91
+ paddingBottom: "8px",
92
+ }}
93
+ >
94
+ <div
95
+ style={{
96
+ borderRadius: "12px",
97
+ border: "1px solid #e4e4e7",
98
+ backgroundColor: "#fafafa",
99
+ padding: "14px 16px",
100
+ }}
101
+ >
102
+ {/* Header row — always visible */}
103
+ <div
104
+ onClick={() => setIsExpanded(!isExpanded)}
105
+ style={{
106
+ display: "flex",
107
+ alignItems: "center",
108
+ justifyContent: "space-between",
109
+ gap: "10px",
110
+ cursor: "pointer",
111
+ userSelect: "none",
112
+ }}
113
+ >
114
+ <div
115
+ style={{
116
+ display: "flex",
117
+ alignItems: "center",
118
+ gap: "8px",
119
+ minWidth: 0,
120
+ }}
121
+ >
122
+ <svg
123
+ style={{
124
+ height: "14px",
125
+ width: "14px",
126
+ color: "#71717a",
127
+ transition: "transform 0.15s",
128
+ transform: isExpanded ? "rotate(90deg)" : "rotate(0deg)",
129
+ flexShrink: 0,
130
+ }}
131
+ fill="none"
132
+ viewBox="0 0 24 24"
133
+ strokeWidth={2}
134
+ stroke="currentColor"
135
+ >
136
+ <path
137
+ strokeLinecap="round"
138
+ strokeLinejoin="round"
139
+ d="M8.25 4.5l7.5 7.5-7.5 7.5"
140
+ />
141
+ </svg>
142
+ <span
143
+ style={{
144
+ display: "inline-block",
145
+ height: "8px",
146
+ width: "8px",
147
+ borderRadius: "50%",
148
+ backgroundColor: dotColor,
149
+ flexShrink: 0,
150
+ }}
151
+ />
152
+ <span
153
+ style={{
154
+ fontSize: "13px",
155
+ fontWeight: 600,
156
+ color: "#18181b",
157
+ overflow: "hidden",
158
+ textOverflow: "ellipsis",
159
+ whiteSpace: "nowrap",
160
+ }}
161
+ >
162
+ {name}
163
+ </span>
164
+ </div>
165
+
166
+ <span
167
+ style={{
168
+ display: "inline-flex",
169
+ alignItems: "center",
170
+ borderRadius: "9999px",
171
+ padding: "2px 8px",
172
+ fontSize: "11px",
173
+ fontWeight: 500,
174
+ backgroundColor: badgeBg,
175
+ color: badgeColor,
176
+ flexShrink: 0,
177
+ }}
178
+ >
179
+ {statusLabel}
180
+ </span>
181
+ </div>
182
+
183
+ {/* Expandable details */}
184
+ {isExpanded && (
185
+ <div style={{ marginTop: "12px", display: "grid", gap: "12px" }}>
186
+ <div>
187
+ <div
188
+ style={{
189
+ fontSize: "10px",
190
+ textTransform: "uppercase",
191
+ letterSpacing: "0.05em",
192
+ color: "#71717a",
193
+ }}
194
+ >
195
+ Arguments
196
+ </div>
197
+ <pre
198
+ style={{
199
+ marginTop: "6px",
200
+ maxHeight: "200px",
201
+ overflow: "auto",
202
+ borderRadius: "6px",
203
+ backgroundColor: "#f4f4f5",
204
+ padding: "10px",
205
+ fontSize: "11px",
206
+ lineHeight: 1.6,
207
+ color: "#27272a",
208
+ whiteSpace: "pre-wrap",
209
+ wordBreak: "break-word",
210
+ }}
211
+ >
212
+ {JSON.stringify(parameters ?? {}, null, 2)}
213
+ </pre>
214
+ </div>
215
+
216
+ {result !== undefined && (
217
+ <div>
218
+ <div
219
+ style={{
220
+ fontSize: "10px",
221
+ textTransform: "uppercase",
222
+ letterSpacing: "0.05em",
223
+ color: "#71717a",
224
+ }}
225
+ >
226
+ Result
227
+ </div>
228
+ <pre
229
+ style={{
230
+ marginTop: "6px",
231
+ maxHeight: "200px",
232
+ overflow: "auto",
233
+ borderRadius: "6px",
234
+ backgroundColor: "#f4f4f5",
235
+ padding: "10px",
236
+ fontSize: "11px",
237
+ lineHeight: 1.6,
238
+ color: "#27272a",
239
+ whiteSpace: "pre-wrap",
240
+ wordBreak: "break-word",
241
+ }}
242
+ >
243
+ {typeof result === "string"
244
+ ? result
245
+ : JSON.stringify(result, null, 2)}
246
+ </pre>
247
+ </div>
248
+ )}
249
+ </div>
250
+ )}
251
+ </div>
252
+ </div>
253
+ );
254
+ }
@@ -0,0 +1,43 @@
1
+ import { useEffect } from "react";
2
+ import { useCopilotKit } from "../providers/CopilotKitProvider";
3
+ import type { ReactFrontendTool } from "../types/frontend-tool";
4
+
5
+ const EMPTY_DEPS: ReadonlyArray<unknown> = [];
6
+
7
+ export function useFrontendTool<
8
+ T extends Record<string, unknown> = Record<string, unknown>,
9
+ >(tool: ReactFrontendTool<T>, deps?: ReadonlyArray<unknown>) {
10
+ const { copilotkit } = useCopilotKit();
11
+ const extraDeps = deps ?? EMPTY_DEPS;
12
+
13
+ useEffect(() => {
14
+ const name = tool.name;
15
+
16
+ // Always register/override the tool for this name on mount
17
+ if (copilotkit.getTool({ toolName: name, agentId: tool.agentId })) {
18
+ console.warn(
19
+ `Tool '${name}' already exists for agent '${tool.agentId || "global"}'. Overriding with latest registration.`,
20
+ );
21
+ copilotkit.removeTool(name, tool.agentId);
22
+ }
23
+ copilotkit.addTool(tool);
24
+
25
+ // Register/override renderer by name and agentId through core
26
+ if (tool.render && tool.parameters) {
27
+ copilotkit.addHookRenderToolCall({
28
+ name,
29
+ args: tool.parameters,
30
+ agentId: tool.agentId,
31
+ render: tool.render,
32
+ });
33
+ }
34
+
35
+ return () => {
36
+ copilotkit.removeTool(name, tool.agentId);
37
+ // we are intentionally not removing the render here so that the tools can still render in the chat history
38
+ };
39
+ // Depend on stable keys by default and allow callers to opt into
40
+ // additional dependencies for dynamic tool configuration.
41
+ // tool.available is included so toggling availability re-registers the tool.
42
+ }, [tool.name, tool.available, copilotkit, extraDeps.length, ...extraDeps]);
43
+ }
@@ -0,0 +1,81 @@
1
+ import { useCopilotKit } from "../providers/CopilotKitProvider";
2
+ import type { ReactFrontendTool } from "../types/frontend-tool";
3
+ import type { ReactHumanInTheLoop } from "../types/human-in-the-loop";
4
+ import type { ReactToolCallRenderer } from "../types/react-tool-call-renderer";
5
+ import { useCallback, useEffect, useRef } from "react";
6
+ import React from "react";
7
+ import { useFrontendTool } from "./use-frontend-tool";
8
+
9
+ export function useHumanInTheLoop<
10
+ T extends Record<string, unknown> = Record<string, unknown>,
11
+ >(tool: ReactHumanInTheLoop<T>, deps?: ReadonlyArray<unknown>) {
12
+ const { copilotkit } = useCopilotKit();
13
+ const resolvePromiseRef = useRef<((result: unknown) => void) | null>(null);
14
+
15
+ const respond = useCallback(async (result: unknown) => {
16
+ if (resolvePromiseRef.current) {
17
+ resolvePromiseRef.current(result);
18
+ resolvePromiseRef.current = null;
19
+ }
20
+ }, []);
21
+
22
+ const handler = useCallback(async () => {
23
+ return new Promise((resolve) => {
24
+ resolvePromiseRef.current = resolve;
25
+ });
26
+ }, []);
27
+
28
+ const RenderComponent: ReactToolCallRenderer<T>["render"] = useCallback(
29
+ (props) => {
30
+ const ToolComponent = tool.render;
31
+
32
+ // Enhance props based on current status
33
+ if (props.status === "inProgress") {
34
+ const enhancedProps = {
35
+ ...props,
36
+ name: tool.name,
37
+ description: tool.description || "",
38
+ respond: undefined,
39
+ };
40
+ return React.createElement(ToolComponent, enhancedProps);
41
+ } else if (props.status === "executing") {
42
+ const enhancedProps = {
43
+ ...props,
44
+ name: tool.name,
45
+ description: tool.description || "",
46
+ respond,
47
+ };
48
+ return React.createElement(ToolComponent, enhancedProps);
49
+ } else if (props.status === "complete") {
50
+ const enhancedProps = {
51
+ ...props,
52
+ name: tool.name,
53
+ description: tool.description || "",
54
+ respond: undefined,
55
+ };
56
+ return React.createElement(ToolComponent, enhancedProps);
57
+ }
58
+
59
+ // Fallback - just render with original props
60
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
+ return React.createElement(ToolComponent, props as any);
62
+ },
63
+ [tool.render, tool.name, tool.description, respond],
64
+ );
65
+
66
+ const frontendTool: ReactFrontendTool<T> = {
67
+ ...tool,
68
+ handler,
69
+ render: RenderComponent,
70
+ };
71
+
72
+ useFrontendTool(frontendTool, deps);
73
+
74
+ // Human-in-the-loop tools should remove their renderer on unmount
75
+ // since they can't respond to user interactions anymore
76
+ useEffect(() => {
77
+ return () => {
78
+ copilotkit.removeHookRenderToolCall(tool.name, tool.agentId);
79
+ };
80
+ }, [copilotkit, tool.name, tool.agentId]);
81
+ }