@copilotkit/react-core 1.56.0 → 1.56.2

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 (36) hide show
  1. package/dist/{copilotkit-Dv8zU8_U.d.cts → copilotkit-BtP7w7cT.d.cts} +10 -1
  2. package/dist/{copilotkit-Dv8zU8_U.d.cts.map → copilotkit-BtP7w7cT.d.cts.map} +1 -1
  3. package/dist/{copilotkit-f2Uq0RwG.d.mts → copilotkit-CCbxm6JM.d.mts} +10 -1
  4. package/dist/{copilotkit-f2Uq0RwG.d.mts.map → copilotkit-CCbxm6JM.d.mts.map} +1 -1
  5. package/dist/{copilotkit-Cvb6WpAX.cjs → copilotkit-CSJw5BG8.cjs} +32 -17
  6. package/dist/copilotkit-CSJw5BG8.cjs.map +1 -0
  7. package/dist/{copilotkit-BebqQrYT.mjs → copilotkit-Cj2ZIxVr.mjs} +32 -17
  8. package/dist/copilotkit-Cj2ZIxVr.mjs.map +1 -0
  9. package/dist/index.cjs +1 -1
  10. package/dist/index.d.cts +1 -1
  11. package/dist/index.d.mts +1 -1
  12. package/dist/index.mjs +1 -1
  13. package/dist/index.umd.js +14 -6
  14. package/dist/index.umd.js.map +1 -1
  15. package/dist/v2/index.cjs +1 -1
  16. package/dist/v2/index.d.cts +1 -1
  17. package/dist/v2/index.d.mts +1 -1
  18. package/dist/v2/index.mjs +1 -1
  19. package/dist/v2/index.umd.js +32 -17
  20. package/dist/v2/index.umd.js.map +1 -1
  21. package/package.json +6 -6
  22. package/src/components/CopilotListeners.tsx +15 -4
  23. package/src/components/__tests__/CopilotListeners.test.tsx +38 -0
  24. package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +4 -4
  25. package/src/v2/components/chat/CopilotChatInput.tsx +21 -2
  26. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.thumbs.test.tsx +72 -0
  27. package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +38 -0
  28. package/src/v2/components/ui/button.tsx +12 -11
  29. package/src/v2/hooks/__tests__/use-render-custom-messages.test.tsx +55 -0
  30. package/src/v2/hooks/use-render-custom-messages.tsx +1 -1
  31. package/src/v2/hooks/use-render-tool-call.tsx +3 -0
  32. package/src/v2/hooks/use-render-tool.tsx +3 -0
  33. package/src/v2/types/defineToolCallRenderer.ts +3 -0
  34. package/src/v2/types/react-tool-call-renderer.ts +3 -0
  35. package/dist/copilotkit-BebqQrYT.mjs.map +0 -1
  36. package/dist/copilotkit-Cvb6WpAX.cjs.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copilotkit/react-core",
3
- "version": "1.56.0",
3
+ "version": "1.56.2",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "ai",
@@ -73,11 +73,11 @@
73
73
  "untruncate-json": "^0.0.1",
74
74
  "use-stick-to-bottom": "^1.1.1",
75
75
  "zod-to-json-schema": "^3.24.5",
76
- "@copilotkit/a2ui-renderer": "1.56.0",
77
- "@copilotkit/core": "1.56.0",
78
- "@copilotkit/runtime-client-gql": "1.56.0",
79
- "@copilotkit/web-inspector": "1.56.0",
80
- "@copilotkit/shared": "1.56.0"
76
+ "@copilotkit/a2ui-renderer": "1.56.2",
77
+ "@copilotkit/shared": "1.56.2",
78
+ "@copilotkit/web-inspector": "1.56.2",
79
+ "@copilotkit/core": "1.56.2",
80
+ "@copilotkit/runtime-client-gql": "1.56.2"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@tailwindcss/cli": "^4.1.11",
@@ -60,16 +60,27 @@ const usePredictStateSubscription = (agent?: AbstractAgent) => {
60
60
  }, [agent, getSubscriber]);
61
61
  };
62
62
 
63
- export function CopilotListeners() {
64
- const { copilotkit } = useCopilotKit();
63
+ function CopilotListenersAgentSubscription() {
65
64
  const existingConfig = useCopilotChatConfiguration();
66
65
  const resolvedAgentId = existingConfig?.agentId;
67
- const { setBannerError } = useToast();
68
66
 
69
67
  const { agent } = useAgent({ agentId: resolvedAgentId });
70
68
 
71
69
  usePredictStateSubscription(agent);
72
70
 
71
+ return null;
72
+ }
73
+
74
+ export function CopilotListeners() {
75
+ const { copilotkit } = useCopilotKit();
76
+ const { setBannerError } = useToast();
77
+
78
+ // Only render the agent subscription when agents are registered or a runtime
79
+ // is configured. Without this guard, useAgent() throws when the agents map is
80
+ // empty and no runtimeUrl is set (#3249).
81
+ const hasAgents = Object.keys(copilotkit.agents ?? {}).length > 0;
82
+ const hasRuntime = copilotkit.runtimeUrl !== undefined;
83
+
73
84
  useEffect(() => {
74
85
  const subscriber: CopilotKitCoreSubscriber = {
75
86
  onError: ({ error, code, context }) => {
@@ -122,5 +133,5 @@ export function CopilotListeners() {
122
133
  };
123
134
  }, [copilotkit?.subscribe]);
124
135
 
125
- return null;
136
+ return hasAgents || hasRuntime ? <CopilotListenersAgentSubscription /> : null;
126
137
  }
@@ -0,0 +1,38 @@
1
+ import React from "react";
2
+ import { describe, it, expect, vi, beforeEach } from "vitest";
3
+ import { render } from "@testing-library/react";
4
+ import { CopilotListeners } from "../CopilotListeners";
5
+ import { CopilotKitProvider } from "../../v2/providers/CopilotKitProvider";
6
+ import { CopilotChatConfigurationProvider } from "../../v2/providers/CopilotChatConfigurationProvider";
7
+ import { ToastProvider } from "../toast/toast-provider";
8
+
9
+ /**
10
+ * Regression test for #3249: CopilotListeners throws when no agents registered.
11
+ *
12
+ * When CopilotKitProvider has no agents registered (empty agents map) and no
13
+ * runtimeUrl, useAgent() inside CopilotListeners throws. The component should
14
+ * handle this gracefully and render null without crashing.
15
+ */
16
+ describe("CopilotListeners (#3249)", () => {
17
+ beforeEach(() => {
18
+ vi.spyOn(console, "error").mockImplementation(() => {});
19
+ });
20
+
21
+ it("does not throw when no agents are registered", () => {
22
+ // No agents, no runtimeUrl - should not crash
23
+ expect(() => {
24
+ render(
25
+ <ToastProvider enabled={false}>
26
+ <CopilotKitProvider>
27
+ <CopilotChatConfigurationProvider
28
+ agentId="default"
29
+ threadId="test-thread"
30
+ >
31
+ <CopilotListeners />
32
+ </CopilotChatConfigurationProvider>
33
+ </CopilotKitProvider>
34
+ </ToastProvider>,
35
+ );
36
+ }).not.toThrow();
37
+ });
38
+ });
@@ -98,7 +98,7 @@ export function CopilotChatAssistantMessage({
98
98
  thumbsUpButton,
99
99
  CopilotChatAssistantMessage.ThumbsUpButton,
100
100
  {
101
- onClick: onThumbsUp,
101
+ onClick: onThumbsUp ? () => onThumbsUp(message) : undefined,
102
102
  },
103
103
  );
104
104
 
@@ -106,7 +106,7 @@ export function CopilotChatAssistantMessage({
106
106
  thumbsDownButton,
107
107
  CopilotChatAssistantMessage.ThumbsDownButton,
108
108
  {
109
- onClick: onThumbsDown,
109
+ onClick: onThumbsDown ? () => onThumbsDown(message) : undefined,
110
110
  },
111
111
  );
112
112
 
@@ -114,7 +114,7 @@ export function CopilotChatAssistantMessage({
114
114
  readAloudButton,
115
115
  CopilotChatAssistantMessage.ReadAloudButton,
116
116
  {
117
- onClick: onReadAloud,
117
+ onClick: onReadAloud ? () => onReadAloud(message) : undefined,
118
118
  },
119
119
  );
120
120
 
@@ -122,7 +122,7 @@ export function CopilotChatAssistantMessage({
122
122
  regenerateButton,
123
123
  CopilotChatAssistantMessage.RegenerateButton,
124
124
  {
125
- onClick: onRegenerate,
125
+ onClick: onRegenerate ? () => onRegenerate(message) : undefined,
126
126
  },
127
127
  );
128
128
 
@@ -384,6 +384,12 @@ export function CopilotChatInput({
384
384
  );
385
385
 
386
386
  const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
387
+ // Skip key handling during IME composition (e.g. CJK input).
388
+ // The compositionend event will fire separately when composition ends.
389
+ if (e.nativeEvent.isComposing || e.keyCode === 229) {
390
+ return;
391
+ }
392
+
387
393
  if (commandQuery !== null && mode === "input") {
388
394
  if (e.key === "ArrowDown") {
389
395
  if (filteredCommands.length > 0) {
@@ -455,10 +461,12 @@ export function CopilotChatInput({
455
461
 
456
462
  onSubmitMessage(trimmed);
457
463
 
464
+ // Always clear the input after sending, including controlled mode.
465
+ // In controlled mode, onChange("") notifies the parent to reset its state.
458
466
  if (!isControlled) {
459
467
  setInternalValue("");
460
- onChange?.("");
461
468
  }
469
+ onChange?.("");
462
470
 
463
471
  if (inputRef.current) {
464
472
  inputRef.current.focus();
@@ -470,6 +478,12 @@ export function CopilotChatInput({
470
478
  value: resolvedValue,
471
479
  onChange: handleChange,
472
480
  onKeyDown: handleKeyDown,
481
+ onCompositionStart: () => {
482
+ isComposingRef.current = true;
483
+ },
484
+ onCompositionEnd: () => {
485
+ isComposingRef.current = false;
486
+ },
473
487
  autoFocus: autoFocus,
474
488
  className: twMerge(
475
489
  "cpk:w-full cpk:py-3",
@@ -612,9 +626,14 @@ export function CopilotChatInput({
612
626
  }
613
627
  };
614
628
 
629
+ // Track whether an IME composition is active so we can avoid
630
+ // resetting textarea.value during measurement (which would break
631
+ // the composition session).
632
+ const isComposingRef = useRef(false);
633
+
615
634
  const ensureMeasurements = useCallback(() => {
616
635
  const textarea = inputRef.current;
617
- if (!textarea) {
636
+ if (!textarea || isComposingRef.current) {
618
637
  return;
619
638
  }
620
639
 
@@ -0,0 +1,72 @@
1
+ import React from "react";
2
+ import { describe, it, expect, vi, beforeEach } from "vitest";
3
+ import { render, screen, fireEvent } from "@testing-library/react";
4
+ import { AssistantMessage } from "@ag-ui/core";
5
+ import { CopilotChatAssistantMessage } from "../CopilotChatAssistantMessage";
6
+ import { CopilotChatConfigurationProvider } from "../../../providers/CopilotChatConfigurationProvider";
7
+ import { CopilotKitProvider } from "../../../providers/CopilotKitProvider";
8
+
9
+ const TEST_THREAD_ID = "test-thread";
10
+
11
+ const renderWithProvider = (component: React.ReactElement) => {
12
+ return render(
13
+ <CopilotKitProvider>
14
+ <CopilotChatConfigurationProvider threadId={TEST_THREAD_ID}>
15
+ {component}
16
+ </CopilotChatConfigurationProvider>
17
+ </CopilotKitProvider>,
18
+ );
19
+ };
20
+
21
+ describe("CopilotChatAssistantMessage thumbs callbacks (#3457)", () => {
22
+ const message: AssistantMessage = {
23
+ id: "msg-1",
24
+ role: "assistant",
25
+ content: "Hello from the assistant",
26
+ };
27
+
28
+ it("onThumbsUp receives AssistantMessage, not SyntheticEvent", () => {
29
+ const onThumbsUp = vi.fn();
30
+
31
+ renderWithProvider(
32
+ <CopilotChatAssistantMessage message={message} onThumbsUp={onThumbsUp} />,
33
+ );
34
+
35
+ const thumbsUpButton = screen.getByRole("button", {
36
+ name: /good response/i,
37
+ });
38
+ fireEvent.click(thumbsUpButton);
39
+
40
+ expect(onThumbsUp).toHaveBeenCalledTimes(1);
41
+ const arg = onThumbsUp.mock.calls[0][0];
42
+ // Should receive AssistantMessage
43
+ expect(arg).toHaveProperty("id", "msg-1");
44
+ expect(arg).toHaveProperty("role", "assistant");
45
+ expect(arg).toHaveProperty("content", "Hello from the assistant");
46
+ // Should NOT receive a SyntheticEvent (which has nativeEvent, target, etc.)
47
+ expect(arg).not.toHaveProperty("nativeEvent");
48
+ });
49
+
50
+ it("onThumbsDown receives AssistantMessage, not SyntheticEvent", () => {
51
+ const onThumbsDown = vi.fn();
52
+
53
+ renderWithProvider(
54
+ <CopilotChatAssistantMessage
55
+ message={message}
56
+ onThumbsDown={onThumbsDown}
57
+ />,
58
+ );
59
+
60
+ const thumbsDownButton = screen.getByRole("button", {
61
+ name: /bad response/i,
62
+ });
63
+ fireEvent.click(thumbsDownButton);
64
+
65
+ expect(onThumbsDown).toHaveBeenCalledTimes(1);
66
+ const arg = onThumbsDown.mock.calls[0][0];
67
+ expect(arg).toHaveProperty("id", "msg-1");
68
+ expect(arg).toHaveProperty("role", "assistant");
69
+ expect(arg).toHaveProperty("content", "Hello from the assistant");
70
+ expect(arg).not.toHaveProperty("nativeEvent");
71
+ });
72
+ });
@@ -985,6 +985,44 @@ describe("CopilotChatInput", () => {
985
985
  expect((input as HTMLTextAreaElement).value).toBe("test message");
986
986
  expect(mockOnSubmitMessage).toHaveBeenCalledWith("test message");
987
987
  });
988
+
989
+ it("calls onChange with empty string after submission in controlled mode", () => {
990
+ const mockOnChange = vi.fn();
991
+ const mockOnSubmitMessage = vi.fn();
992
+
993
+ const { container } = renderWithProvider(
994
+ <CopilotChatInput
995
+ value="test message"
996
+ onChange={mockOnChange}
997
+ onSubmitMessage={mockOnSubmitMessage}
998
+ />,
999
+ );
1000
+
1001
+ const sendButton = getSendButton(container);
1002
+ fireEvent.click(sendButton!);
1003
+
1004
+ expect(mockOnSubmitMessage).toHaveBeenCalledWith("test message");
1005
+ expect(mockOnChange).toHaveBeenCalledWith("");
1006
+ });
1007
+
1008
+ it("calls onChange with empty string after Enter submission in controlled mode", () => {
1009
+ const mockOnChange = vi.fn();
1010
+ const mockOnSubmitMessage = vi.fn();
1011
+
1012
+ renderWithProvider(
1013
+ <CopilotChatInput
1014
+ value="hello world"
1015
+ onChange={mockOnChange}
1016
+ onSubmitMessage={mockOnSubmitMessage}
1017
+ />,
1018
+ );
1019
+
1020
+ const input = screen.getByRole("textbox");
1021
+ fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
1022
+
1023
+ expect(mockOnSubmitMessage).toHaveBeenCalledWith("hello world");
1024
+ expect(mockOnChange).toHaveBeenCalledWith("");
1025
+ });
988
1026
  });
989
1027
 
990
1028
  describe("Container dimension cache", () => {
@@ -99,25 +99,26 @@ const buttonVariants = cva(
99
99
  },
100
100
  );
101
101
 
102
- function Button({
103
- className,
104
- variant,
105
- size,
106
- asChild = false,
107
- ...props
108
- }: React.ComponentProps<"button"> &
109
- VariantProps<typeof buttonVariants> & {
110
- asChild?: boolean;
111
- }) {
102
+ const Button = React.forwardRef<
103
+ HTMLButtonElement,
104
+ React.ComponentProps<"button"> &
105
+ VariantProps<typeof buttonVariants> & {
106
+ asChild?: boolean;
107
+ }
108
+ >(function Button(
109
+ { className, variant, size, asChild = false, ...props },
110
+ ref,
111
+ ) {
112
112
  const Comp = asChild ? Slot : "button";
113
113
 
114
114
  return (
115
115
  <Comp
116
+ ref={ref}
116
117
  data-slot="button"
117
118
  className={cn(buttonVariants({ variant, size, className }))}
118
119
  {...props}
119
120
  />
120
121
  );
121
- }
122
+ });
122
123
 
123
124
  export { Button, buttonVariants };
@@ -0,0 +1,55 @@
1
+ import React from "react";
2
+ import { describe, it, expect, vi } from "vitest";
3
+ import { renderHook } from "@testing-library/react";
4
+ import { useRenderCustomMessages } from "../use-render-custom-messages";
5
+ import { CopilotKitProvider } from "../../providers/CopilotKitProvider";
6
+ import { CopilotChatConfigurationProvider } from "../../providers/CopilotChatConfigurationProvider";
7
+
8
+ /**
9
+ * Regression test for #3497: useRenderCustomMessages throws "Agent not found"
10
+ * when the agent is undefined during the connecting state.
11
+ *
12
+ * During initial connection, the agent may not yet be registered in the
13
+ * CopilotKit registry. The hook should return null gracefully instead of
14
+ * throwing an error.
15
+ */
16
+ describe("useRenderCustomMessages (#3497)", () => {
17
+ it("returns null instead of throwing when agent is not found", () => {
18
+ // Render the hook inside a CopilotKitProvider with an agentId that
19
+ // does NOT exist in the registry (simulating connecting state).
20
+ // The hook should not throw.
21
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
22
+ <CopilotKitProvider
23
+ renderCustomMessages={[
24
+ {
25
+ agentId: "nonexistent-agent",
26
+ render: () => <div>Custom</div>,
27
+ },
28
+ ]}
29
+ >
30
+ <CopilotChatConfigurationProvider
31
+ agentId="nonexistent-agent"
32
+ threadId="test-thread"
33
+ >
34
+ {children}
35
+ </CopilotChatConfigurationProvider>
36
+ </CopilotKitProvider>
37
+ );
38
+
39
+ const { result } = renderHook(() => useRenderCustomMessages(), { wrapper });
40
+
41
+ // The hook should return a function (the render function), not throw
42
+ // When called, it should handle missing agent gracefully
43
+ if (typeof result.current === "function") {
44
+ const output = result.current({
45
+ message: { id: "msg-1", role: "assistant", content: "test" },
46
+ position: "after",
47
+ });
48
+ // Should return null since agent isn't found
49
+ expect(output).toBeNull();
50
+ } else {
51
+ // If result.current is null, that's also acceptable
52
+ expect(result.current).toBeNull();
53
+ }
54
+ });
55
+ });
@@ -45,7 +45,7 @@ export function useRenderCustomMessages() {
45
45
  const registryAgent = copilotkit.getAgent(agentId);
46
46
  const agent = getThreadClone(registryAgent, threadId) ?? registryAgent;
47
47
  if (!agent) {
48
- throw new Error("Agent not found");
48
+ return null;
49
49
  }
50
50
 
51
51
  const messagesIdsInRun = resolvedRunId
@@ -47,6 +47,7 @@ const ToolCallRenderer = React.memo(
47
47
  return (
48
48
  <RenderComponent
49
49
  name={toolName}
50
+ toolCallId={toolCall.id}
50
51
  args={args}
51
52
  status={ToolCallStatus.Complete}
52
53
  result={toolMessage.content}
@@ -56,6 +57,7 @@ const ToolCallRenderer = React.memo(
56
57
  return (
57
58
  <RenderComponent
58
59
  name={toolName}
60
+ toolCallId={toolCall.id}
59
61
  args={args}
60
62
  status={ToolCallStatus.Executing}
61
63
  result={undefined}
@@ -65,6 +67,7 @@ const ToolCallRenderer = React.memo(
65
67
  return (
66
68
  <RenderComponent
67
69
  name={toolName}
70
+ toolCallId={toolCall.id}
68
71
  args={args}
69
72
  status={ToolCallStatus.InProgress}
70
73
  result={undefined}
@@ -7,6 +7,7 @@ const EMPTY_DEPS: ReadonlyArray<unknown> = [];
7
7
 
8
8
  export interface RenderToolInProgressProps<S extends StandardSchemaV1> {
9
9
  name: string;
10
+ toolCallId: string;
10
11
  parameters: Partial<InferSchemaOutput<S>>;
11
12
  status: "inProgress";
12
13
  result: undefined;
@@ -14,6 +15,7 @@ export interface RenderToolInProgressProps<S extends StandardSchemaV1> {
14
15
 
15
16
  export interface RenderToolExecutingProps<S extends StandardSchemaV1> {
16
17
  name: string;
18
+ toolCallId: string;
17
19
  parameters: InferSchemaOutput<S>;
18
20
  status: "executing";
19
21
  result: undefined;
@@ -21,6 +23,7 @@ export interface RenderToolExecutingProps<S extends StandardSchemaV1> {
21
23
 
22
24
  export interface RenderToolCompleteProps<S extends StandardSchemaV1> {
23
25
  name: string;
26
+ toolCallId: string;
24
27
  parameters: InferSchemaOutput<S>;
25
28
  status: "complete";
26
29
  result: string;
@@ -14,18 +14,21 @@ import { ToolCallStatus } from "@copilotkit/core";
14
14
  type RenderProps<T> =
15
15
  | {
16
16
  name: string;
17
+ toolCallId: string;
17
18
  args: Partial<T>;
18
19
  status: ToolCallStatus.InProgress;
19
20
  result: undefined;
20
21
  }
21
22
  | {
22
23
  name: string;
24
+ toolCallId: string;
23
25
  args: T;
24
26
  status: ToolCallStatus.Executing;
25
27
  result: undefined;
26
28
  }
27
29
  | {
28
30
  name: string;
31
+ toolCallId: string;
29
32
  args: T;
30
33
  status: ToolCallStatus.Complete;
31
34
  result: string;
@@ -12,18 +12,21 @@ export interface ReactToolCallRenderer<T = unknown> {
12
12
  render: React.ComponentType<
13
13
  | {
14
14
  name: string;
15
+ toolCallId: string;
15
16
  args: Partial<T>;
16
17
  status: ToolCallStatus.InProgress;
17
18
  result: undefined;
18
19
  }
19
20
  | {
20
21
  name: string;
22
+ toolCallId: string;
21
23
  args: T;
22
24
  status: ToolCallStatus.Executing;
23
25
  result: undefined;
24
26
  }
25
27
  | {
26
28
  name: string;
29
+ toolCallId: string;
27
30
  args: T;
28
31
  status: ToolCallStatus.Complete;
29
32
  result: string;