@copilotkit/react-core 1.57.1 → 1.57.3

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 (41) hide show
  1. package/dist/{copilotkit-sQWiKtxA.d.cts → copilotkit-BK9CVq9A.d.cts} +6 -1
  2. package/dist/{copilotkit-sQWiKtxA.d.cts.map → copilotkit-BK9CVq9A.d.cts.map} +1 -1
  3. package/dist/{copilotkit-DjxXMYHG.mjs → copilotkit-CC8DjOiC.mjs} +404 -367
  4. package/dist/copilotkit-CC8DjOiC.mjs.map +1 -0
  5. package/dist/{copilotkit-C3k13WZn.cjs → copilotkit-CtXcs1ea.cjs} +403 -366
  6. package/dist/copilotkit-CtXcs1ea.cjs.map +1 -0
  7. package/dist/{copilotkit-BN4I_y1n.d.mts → copilotkit-WlmeVijs.d.mts} +6 -1
  8. package/dist/{copilotkit-BN4I_y1n.d.mts.map → copilotkit-WlmeVijs.d.mts.map} +1 -1
  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 +191 -15
  14. package/dist/index.umd.js.map +1 -1
  15. package/dist/v2/headless.cjs +110 -3
  16. package/dist/v2/headless.cjs.map +1 -1
  17. package/dist/v2/headless.d.cts +142 -2
  18. package/dist/v2/headless.d.cts.map +1 -1
  19. package/dist/v2/headless.d.mts +142 -1
  20. package/dist/v2/headless.d.mts.map +1 -1
  21. package/dist/v2/headless.mjs +108 -4
  22. package/dist/v2/headless.mjs.map +1 -1
  23. package/dist/v2/index.cjs +1 -1
  24. package/dist/v2/index.css +1 -1
  25. package/dist/v2/index.d.cts +1 -1
  26. package/dist/v2/index.d.mts +1 -1
  27. package/dist/v2/index.mjs +1 -1
  28. package/dist/v2/index.umd.js +403 -364
  29. package/dist/v2/index.umd.js.map +1 -1
  30. package/package.json +6 -6
  31. package/src/v2/components/chat/CopilotSidebar.tsx +5 -1
  32. package/src/v2/components/chat/CopilotSidebarView.tsx +24 -10
  33. package/src/v2/components/chat/__tests__/CopilotChatPerf.e2e.test.tsx +68 -0
  34. package/src/v2/components/chat/__tests__/CopilotSidebarView.position.test.tsx +159 -0
  35. package/src/v2/headless.ts +23 -1
  36. package/src/v2/hooks/__tests__/use-component.test.tsx +4 -1
  37. package/src/v2/hooks/use-component.tsx +2 -0
  38. package/src/v2/hooks/use-default-render-tool.tsx +18 -1
  39. package/src/v2/hooks/use-render-tool-call.tsx +35 -5
  40. package/dist/copilotkit-C3k13WZn.cjs.map +0 -1
  41. package/dist/copilotkit-DjxXMYHG.mjs.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copilotkit/react-core",
3
- "version": "1.57.1",
3
+ "version": "1.57.3",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "ai",
@@ -81,11 +81,11 @@
81
81
  "untruncate-json": "^0.0.1",
82
82
  "use-stick-to-bottom": "^1.1.1",
83
83
  "zod-to-json-schema": "^3.24.5",
84
- "@copilotkit/a2ui-renderer": "1.57.1",
85
- "@copilotkit/core": "1.57.1",
86
- "@copilotkit/web-inspector": "1.57.1",
87
- "@copilotkit/runtime-client-gql": "1.57.1",
88
- "@copilotkit/shared": "1.57.1"
84
+ "@copilotkit/a2ui-renderer": "1.57.3",
85
+ "@copilotkit/core": "1.57.3",
86
+ "@copilotkit/runtime-client-gql": "1.57.3",
87
+ "@copilotkit/shared": "1.57.3",
88
+ "@copilotkit/web-inspector": "1.57.3"
89
89
  },
90
90
  "devDependencies": {
91
91
  "@tailwindcss/cli": "^4.1.11",
@@ -14,6 +14,7 @@ export type CopilotSidebarProps = Omit<CopilotChatProps, "chatView"> & {
14
14
  toggleButton?: CopilotSidebarViewProps["toggleButton"];
15
15
  defaultOpen?: boolean;
16
16
  width?: number | string;
17
+ position?: CopilotSidebarViewProps["position"];
17
18
  };
18
19
 
19
20
  export function CopilotSidebar({
@@ -21,6 +22,7 @@ export function CopilotSidebar({
21
22
  toggleButton,
22
23
  defaultOpen,
23
24
  width,
25
+ position,
24
26
  ...chatProps
25
27
  }: CopilotSidebarProps) {
26
28
  const { checkFeature } = useLicenseContext();
@@ -41,6 +43,7 @@ export function CopilotSidebar({
41
43
  toggleButton: viewToggleButton,
42
44
  width: viewWidth,
43
45
  defaultOpen: viewDefaultOpen,
46
+ position: viewPosition,
44
47
  ...restProps
45
48
  } = viewProps as CopilotSidebarViewProps;
46
49
 
@@ -51,12 +54,13 @@ export function CopilotSidebar({
51
54
  toggleButton={toggleButton ?? viewToggleButton}
52
55
  width={width ?? viewWidth}
53
56
  defaultOpen={defaultOpen ?? viewDefaultOpen}
57
+ position={position ?? viewPosition}
54
58
  />
55
59
  );
56
60
  };
57
61
 
58
62
  return Object.assign(Component, CopilotChatView);
59
- }, [header, toggleButton, width, defaultOpen]);
63
+ }, [header, toggleButton, width, defaultOpen, position]);
60
64
 
61
65
  return (
62
66
  <>
@@ -21,6 +21,7 @@ export type CopilotSidebarViewProps = CopilotChatViewProps & {
21
21
  toggleButton?: SlotValue<typeof CopilotChatToggleButton>;
22
22
  width?: number | string;
23
23
  defaultOpen?: boolean;
24
+ position?: "left" | "right";
24
25
  };
25
26
 
26
27
  export function CopilotSidebarView({
@@ -28,6 +29,7 @@ export function CopilotSidebarView({
28
29
  toggleButton,
29
30
  width,
30
31
  defaultOpen = true,
32
+ position = "right",
31
33
  ...props
32
34
  }: CopilotSidebarViewProps) {
33
35
  return (
@@ -36,6 +38,7 @@ export function CopilotSidebarView({
36
38
  header={header}
37
39
  toggleButton={toggleButton}
38
40
  width={width}
41
+ position={position}
39
42
  {...props}
40
43
  />
41
44
  </CopilotChatConfigurationProvider>
@@ -46,6 +49,7 @@ function CopilotSidebarViewInternal({
46
49
  header,
47
50
  toggleButton,
48
51
  width,
52
+ position = "right",
49
53
  ...props
50
54
  }: Omit<CopilotSidebarViewProps, "defaultOpen">) {
51
55
  const configuration = useCopilotChatConfiguration();
@@ -118,29 +122,34 @@ function CopilotSidebarViewInternal({
118
122
  return;
119
123
  if (!window.matchMedia("(min-width: 768px)").matches) return;
120
124
 
125
+ const marginStyleProp =
126
+ position === "left" ? "marginInlineStart" : "marginInlineEnd";
127
+ const transitionCssProp =
128
+ position === "left" ? "margin-inline-start" : "margin-inline-end";
129
+
121
130
  if (isSidebarOpen) {
122
131
  if (hasMounted.current) {
123
- document.body.style.transition = `margin-inline-end ${SIDEBAR_TRANSITION_MS}ms ease`;
132
+ document.body.style.transition = `${transitionCssProp} ${SIDEBAR_TRANSITION_MS}ms ease`;
124
133
  }
125
- document.body.style.marginInlineEnd = widthToMargin(sidebarWidth);
134
+ document.body.style[marginStyleProp] = widthToMargin(sidebarWidth);
126
135
  } else if (hasMounted.current) {
127
- document.body.style.transition = `margin-inline-end ${SIDEBAR_TRANSITION_MS}ms ease`;
128
- document.body.style.marginInlineEnd = "";
136
+ document.body.style.transition = `${transitionCssProp} ${SIDEBAR_TRANSITION_MS}ms ease`;
137
+ document.body.style[marginStyleProp] = "";
129
138
  }
130
139
 
131
140
  hasMounted.current = true;
132
141
 
133
142
  return () => {
134
- document.body.style.marginInlineEnd = "";
143
+ document.body.style[marginStyleProp] = "";
135
144
  document.body.style.transition = "";
136
145
  };
137
- }, [isSidebarOpen, sidebarWidth]);
146
+ }, [isSidebarOpen, sidebarWidth, position]);
138
147
 
139
148
  const headerElement = renderSlot(header, CopilotModalHeader, {});
140
149
  const toggleButtonElement = renderSlot(
141
150
  toggleButton,
142
151
  CopilotChatToggleButton,
143
- {},
152
+ position === "left" ? { className: "cpk:left-6 cpk:right-auto" } : {},
144
153
  );
145
154
 
146
155
  return (
@@ -151,18 +160,23 @@ function CopilotSidebarViewInternal({
151
160
  data-copilotkit
152
161
  data-testid="copilot-sidebar"
153
162
  data-copilot-sidebar
163
+ data-position={position}
154
164
  className={cn(
155
165
  "copilotKitSidebar copilotKitWindow",
156
- "cpk:fixed cpk:right-0 cpk:top-0 cpk:z-[1200] cpk:flex",
166
+ "cpk:fixed cpk:top-0 cpk:z-[1200] cpk:flex",
167
+ position === "left" ? "cpk:left-0" : "cpk:right-0",
157
168
  // Height with dvh fallback and safe area support
158
169
  "cpk:h-[100vh] cpk:h-[100dvh] cpk:max-h-screen",
159
170
  // Responsive width: full on mobile, custom on desktop
160
171
  "cpk:w-full",
161
- "cpk:border-l cpk:border-border cpk:bg-background cpk:text-foreground cpk:shadow-xl",
172
+ position === "left" ? "cpk:border-r" : "cpk:border-l",
173
+ "cpk:border-border cpk:bg-background cpk:text-foreground cpk:shadow-xl",
162
174
  "cpk:transition-transform cpk:duration-300 cpk:ease-out",
163
175
  isSidebarOpen
164
176
  ? "cpk:translate-x-0"
165
- : "cpk:translate-x-full cpk:pointer-events-none",
177
+ : position === "left"
178
+ ? "cpk:-translate-x-full cpk:pointer-events-none"
179
+ : "cpk:translate-x-full cpk:pointer-events-none",
166
180
  )}
167
181
  style={
168
182
  {
@@ -115,6 +115,74 @@ describe("CopilotChat perf — re-render regression", () => {
115
115
  renderCounts.clear();
116
116
  });
117
117
 
118
+ // @tanstack/virtual-core 3.13.18 has a latent bug: `scrollToIndex` schedules
119
+ // a nested rAF that calls `this.targetWindow.requestAnimationFrame(verify)`
120
+ // with no null-check. The virtualizer's cleanup (run on React unmount) sets
121
+ // `targetWindow = null`, so if the outer rAF fires after unmount, the inner
122
+ // schedule throws `Cannot read properties of null (reading 'requestAnimationFrame')`.
123
+ //
124
+ // Vitest reports this as an unhandled error and exits non-zero even though
125
+ // every test passes. We wrap rAF (on BOTH globalThis and window — they're
126
+ // separate bindings in vitest+jsdom; tanstack uses `targetWindow.rAF` which
127
+ // resolves to `window.rAF`) so callbacks that hit the known tanstack bug
128
+ // are swallowed. After each test we drain pending callbacks and replace rAF
129
+ // with a no-op to keep stragglers from leaking into the next test.
130
+ const realRAF = globalThis.requestAnimationFrame;
131
+ const realWindowRAF =
132
+ typeof window !== "undefined" ? window.requestAnimationFrame : realRAF;
133
+
134
+ const isKnownTanstackTeardownError = (err: unknown): boolean => {
135
+ const message =
136
+ (err as { message?: string } | null | undefined)?.message ?? "";
137
+ return (
138
+ message.includes("Cannot read properties of null") &&
139
+ message.includes("requestAnimationFrame")
140
+ );
141
+ };
142
+
143
+ const wrapRAF = (real: typeof requestAnimationFrame) =>
144
+ ((cb: FrameRequestCallback) =>
145
+ real((time) => {
146
+ try {
147
+ cb(time);
148
+ } catch (err) {
149
+ if (isKnownTanstackTeardownError(err)) return;
150
+ throw err;
151
+ }
152
+ })) as typeof requestAnimationFrame;
153
+
154
+ const noopRAF = ((_cb: FrameRequestCallback) =>
155
+ 0) as typeof requestAnimationFrame;
156
+
157
+ const installSafeRAF = () => {
158
+ globalThis.requestAnimationFrame = wrapRAF(realRAF);
159
+ if (typeof window !== "undefined") {
160
+ window.requestAnimationFrame = wrapRAF(realWindowRAF);
161
+ }
162
+ };
163
+
164
+ const installNoopRAF = () => {
165
+ globalThis.requestAnimationFrame = noopRAF;
166
+ if (typeof window !== "undefined") {
167
+ window.requestAnimationFrame = noopRAF;
168
+ }
169
+ };
170
+
171
+ afterEach(async () => {
172
+ // Best-effort drain of already-queued rAF callbacks.
173
+ await act(async () => {
174
+ await new Promise<void>((r) => realRAF(() => r()));
175
+ await new Promise<void>((r) => realRAF(() => r()));
176
+ });
177
+ // Neuter rAF so callbacks scheduled during RTL cleanup are harmless.
178
+ installNoopRAF();
179
+ });
180
+
181
+ beforeEach(() => {
182
+ // Install the error-swallowing wrap on rAF for the upcoming test.
183
+ installSafeRAF();
184
+ });
185
+
118
186
  it("completed messages do not re-render when a new message is added", async () => {
119
187
  const agent = new MockStepwiseAgent();
120
188
  renderWithSpy(agent);
@@ -0,0 +1,159 @@
1
+ import React from "react";
2
+ import { render } from "@testing-library/react";
3
+ import { describe, it, expect } from "vitest";
4
+ import { CopilotSidebarView } from "../CopilotSidebarView";
5
+ import { CopilotSidebar } from "../CopilotSidebar";
6
+ import { CopilotKitProvider } from "../../../providers/CopilotKitProvider";
7
+ import { CopilotChatConfigurationProvider } from "../../../providers/CopilotChatConfigurationProvider";
8
+ import {
9
+ MockStepwiseAgent,
10
+ renderWithCopilotKit,
11
+ } from "../../../__tests__/utils/test-helpers";
12
+
13
+ const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
14
+ <CopilotKitProvider>
15
+ <CopilotChatConfigurationProvider threadId="test-thread">
16
+ {children}
17
+ </CopilotChatConfigurationProvider>
18
+ </CopilotKitProvider>
19
+ );
20
+
21
+ const sampleMessages = [{ id: "1", role: "user" as const, content: "Hello" }];
22
+
23
+ function getSidebarAside(container: HTMLElement) {
24
+ const sidebar = container.querySelector("[data-copilot-sidebar]");
25
+ if (!sidebar) throw new Error("sidebar aside not found");
26
+ return sidebar;
27
+ }
28
+
29
+ describe("CopilotSidebarView position prop", () => {
30
+ describe("CopilotSidebarView", () => {
31
+ it("defaults to right-anchored when position is omitted", () => {
32
+ const { container } = render(
33
+ <TestWrapper>
34
+ <CopilotSidebarView messages={sampleMessages} />
35
+ </TestWrapper>,
36
+ );
37
+
38
+ const aside = getSidebarAside(container);
39
+ expect(aside.classList.contains("cpk:right-0")).toBe(true);
40
+ expect(aside.classList.contains("cpk:border-l")).toBe(true);
41
+ expect(aside.classList.contains("cpk:left-0")).toBe(false);
42
+ expect(aside.classList.contains("cpk:border-r")).toBe(false);
43
+ });
44
+
45
+ it('renders right-anchored when position="right" explicitly', () => {
46
+ const { container } = render(
47
+ <TestWrapper>
48
+ <CopilotSidebarView messages={sampleMessages} position="right" />
49
+ </TestWrapper>,
50
+ );
51
+
52
+ const aside = getSidebarAside(container);
53
+ expect(aside.classList.contains("cpk:right-0")).toBe(true);
54
+ expect(aside.classList.contains("cpk:border-l")).toBe(true);
55
+ expect(aside.classList.contains("cpk:left-0")).toBe(false);
56
+ expect(aside.classList.contains("cpk:border-r")).toBe(false);
57
+ });
58
+
59
+ it('renders left-anchored when position="left"', () => {
60
+ const { container } = render(
61
+ <TestWrapper>
62
+ <CopilotSidebarView messages={sampleMessages} position="left" />
63
+ </TestWrapper>,
64
+ );
65
+
66
+ const aside = getSidebarAside(container);
67
+ expect(aside.classList.contains("cpk:left-0")).toBe(true);
68
+ expect(aside.classList.contains("cpk:border-r")).toBe(true);
69
+ expect(aside.classList.contains("cpk:right-0")).toBe(false);
70
+ expect(aside.classList.contains("cpk:border-l")).toBe(false);
71
+ });
72
+
73
+ it('translates off-screen to the right when closed and position="right"', () => {
74
+ const { container } = render(
75
+ <TestWrapper>
76
+ <CopilotSidebarView
77
+ messages={sampleMessages}
78
+ defaultOpen={false}
79
+ position="right"
80
+ />
81
+ </TestWrapper>,
82
+ );
83
+
84
+ const aside = getSidebarAside(container);
85
+ expect(aside.classList.contains("cpk:translate-x-full")).toBe(true);
86
+ expect(aside.classList.contains("cpk:-translate-x-full")).toBe(false);
87
+ });
88
+
89
+ it('translates off-screen to the left when closed and position="left"', () => {
90
+ const { container } = render(
91
+ <TestWrapper>
92
+ <CopilotSidebarView
93
+ messages={sampleMessages}
94
+ defaultOpen={false}
95
+ position="left"
96
+ />
97
+ </TestWrapper>,
98
+ );
99
+
100
+ const aside = getSidebarAside(container);
101
+ expect(aside.classList.contains("cpk:-translate-x-full")).toBe(true);
102
+ expect(aside.classList.contains("cpk:translate-x-full")).toBe(false);
103
+ });
104
+
105
+ it('anchors the toggle button to the left when position="left"', () => {
106
+ const { container } = render(
107
+ <TestWrapper>
108
+ <CopilotSidebarView messages={sampleMessages} position="left" />
109
+ </TestWrapper>,
110
+ );
111
+
112
+ const toggle = container.querySelector(
113
+ '[data-slot="chat-toggle-button"]',
114
+ );
115
+ if (!toggle) throw new Error("toggle button not found");
116
+ expect(toggle.classList.contains("cpk:left-6")).toBe(true);
117
+ expect(toggle.classList.contains("cpk:right-auto")).toBe(true);
118
+ });
119
+
120
+ it("keeps the toggle button right-anchored by default", () => {
121
+ const { container } = render(
122
+ <TestWrapper>
123
+ <CopilotSidebarView messages={sampleMessages} />
124
+ </TestWrapper>,
125
+ );
126
+
127
+ const toggle = container.querySelector(
128
+ '[data-slot="chat-toggle-button"]',
129
+ );
130
+ if (!toggle) throw new Error("toggle button not found");
131
+ expect(toggle.classList.contains("cpk:right-6")).toBe(true);
132
+ expect(toggle.classList.contains("cpk:left-6")).toBe(false);
133
+ });
134
+ });
135
+
136
+ describe("CopilotSidebar wrapper", () => {
137
+ it('forwards position="left" through to CopilotSidebarView', () => {
138
+ const { container } = renderWithCopilotKit({
139
+ agent: new MockStepwiseAgent(),
140
+ children: <CopilotSidebar position="left" />,
141
+ });
142
+
143
+ const aside = getSidebarAside(container);
144
+ expect(aside.classList.contains("cpk:left-0")).toBe(true);
145
+ expect(aside.classList.contains("cpk:border-r")).toBe(true);
146
+ });
147
+
148
+ it("defaults to right-anchored when position is omitted", () => {
149
+ const { container } = renderWithCopilotKit({
150
+ agent: new MockStepwiseAgent(),
151
+ children: <CopilotSidebar />,
152
+ });
153
+
154
+ const aside = getSidebarAside(container);
155
+ expect(aside.classList.contains("cpk:right-0")).toBe(true);
156
+ expect(aside.classList.contains("cpk:border-l")).toBe(true);
157
+ });
158
+ });
159
+ });
@@ -26,7 +26,13 @@ export { useAgent, type UseAgentUpdate } from "./hooks/use-agent";
26
26
  export { useFrontendTool } from "./hooks/use-frontend-tool";
27
27
  export { useComponent } from "./hooks/use-component";
28
28
  export { useHumanInTheLoop } from "./hooks/use-human-in-the-loop";
29
- export { useInterrupt, type UseInterruptConfig } from "./hooks/use-interrupt";
29
+ export {
30
+ useInterrupt,
31
+ type UseInterruptConfig,
32
+ type InterruptEvent,
33
+ type InterruptHandlerProps,
34
+ type InterruptRenderProps,
35
+ } from "./hooks/use-interrupt";
30
36
  export { useSuggestions } from "./hooks/use-suggestions";
31
37
  export { useConfigureSuggestions } from "./hooks/use-configure-suggestions";
32
38
  export {
@@ -40,3 +46,19 @@ export {
40
46
  type UseThreadsInput,
41
47
  type UseThreadsResult,
42
48
  } from "./hooks/use-threads";
49
+
50
+ export {
51
+ useRenderTool,
52
+ type RenderToolProps,
53
+ type RenderToolInProgressProps,
54
+ type RenderToolExecutingProps,
55
+ type RenderToolCompleteProps,
56
+ } from "./hooks/use-render-tool";
57
+ export { defineToolCallRenderer } from "./types/defineToolCallRenderer";
58
+
59
+ // Platform-agnostic types
60
+ export type { ReactFrontendTool } from "./types/frontend-tool";
61
+ export type { ReactHumanInTheLoop } from "./types/human-in-the-loop";
62
+
63
+ // Platform-agnostic capability introspection
64
+ export { useCapabilities } from "./hooks/use-capabilities";
@@ -45,7 +45,7 @@ describe("useComponent", () => {
45
45
  );
46
46
  });
47
47
 
48
- it("appends custom description and forwards parameters, agentId, and deps", () => {
48
+ it("appends custom description and forwards parameters, agentId, deps, and followUp", () => {
49
49
  const weatherSchema = z.object({
50
50
  city: z.string(),
51
51
  unit: z.enum(["c", "f"]),
@@ -65,6 +65,7 @@ describe("useComponent", () => {
65
65
  parameters: weatherSchema,
66
66
  render: DemoComponent,
67
67
  agentId: "weather-agent",
68
+ followUp: false,
68
69
  },
69
70
  deps,
70
71
  );
@@ -79,6 +80,7 @@ describe("useComponent", () => {
79
80
  description: string;
80
81
  parameters: typeof weatherSchema;
81
82
  agentId?: string;
83
+ followUp?: boolean;
82
84
  },
83
85
  ReadonlyArray<unknown>,
84
86
  ];
@@ -91,6 +93,7 @@ describe("useComponent", () => {
91
93
  );
92
94
  expect(toolConfig.parameters).toBe(weatherSchema);
93
95
  expect(toolConfig.agentId).toBe("weather-agent");
96
+ expect(toolConfig.followUp).toBe(false);
94
97
  expect(forwardedDeps).toBe(deps);
95
98
  });
96
99
 
@@ -65,6 +65,7 @@ export function useComponent<
65
65
  parameters?: TSchema;
66
66
  render: ComponentType<NoInfer<InferRenderProps<TSchema>>>;
67
67
  agentId?: string;
68
+ followUp?: boolean;
68
69
  },
69
70
  deps?: ReadonlyArray<unknown>,
70
71
  ): void {
@@ -83,6 +84,7 @@ export function useComponent<
83
84
  return <Component {...(args as InferRenderProps<TSchema>)} />;
84
85
  },
85
86
  agentId: config.agentId,
87
+ followUp: config.followUp,
86
88
  },
87
89
  deps,
88
90
  );
@@ -63,7 +63,7 @@ export function useDefaultRenderTool(
63
63
  );
64
64
  }
65
65
 
66
- function DefaultToolCallRenderer({
66
+ export function DefaultToolCallRenderer({
67
67
  name,
68
68
  parameters,
69
69
  status,
@@ -86,6 +86,11 @@ function DefaultToolCallRenderer({
86
86
 
87
87
  return (
88
88
  <div
89
+ data-testid="copilot-tool-render"
90
+ data-tool-name={name}
91
+ data-status={statusString}
92
+ data-args={safeStringifyForAttr(parameters)}
93
+ data-result={safeStringifyForAttr(result)}
89
94
  style={{
90
95
  marginTop: "8px",
91
96
  paddingBottom: "8px",
@@ -150,6 +155,7 @@ function DefaultToolCallRenderer({
150
155
  }}
151
156
  />
152
157
  <span
158
+ data-testid="copilot-tool-render-name"
153
159
  style={{
154
160
  fontSize: "13px",
155
161
  fontWeight: 600,
@@ -164,6 +170,7 @@ function DefaultToolCallRenderer({
164
170
  </div>
165
171
 
166
172
  <span
173
+ data-testid="copilot-tool-render-status"
167
174
  style={{
168
175
  display: "inline-flex",
169
176
  alignItems: "center",
@@ -252,3 +259,13 @@ function DefaultToolCallRenderer({
252
259
  </div>
253
260
  );
254
261
  }
262
+
263
+ function safeStringifyForAttr(value: unknown): string {
264
+ if (value === undefined || value === null) return "";
265
+ if (typeof value === "string") return value;
266
+ try {
267
+ return JSON.stringify(value);
268
+ } catch {
269
+ return String(value);
270
+ }
271
+ }
@@ -6,6 +6,7 @@ import { useCopilotChatConfiguration } from "../providers/CopilotChatConfigurati
6
6
  import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
7
7
  import { partialJSONParse } from "@copilotkit/shared";
8
8
  import { ReactToolCallRenderer } from "../types/react-tool-call-renderer";
9
+ import { DefaultToolCallRenderer } from "./use-default-render-tool";
9
10
 
10
11
  export interface UseRenderToolCallProps {
11
12
  toolCall: ToolCall;
@@ -153,11 +154,12 @@ export function useRenderToolCall() {
153
154
  exactMatches[0] ||
154
155
  renderToolCalls.find((rc) => rc.name === "*");
155
156
 
156
- if (!renderConfig) {
157
- return null;
158
- }
159
-
160
- const RenderComponent = renderConfig.render;
157
+ // Fall back to the framework's built-in default tool-call renderer
158
+ // when neither a per-tool nor a wildcard renderer has been
159
+ // registered. This makes "zero custom renderers" demos paint tool
160
+ // calls out-of-the-box instead of going invisible.
161
+ const RenderComponent = (renderConfig?.render ??
162
+ defaultToolCallRenderAdapter) as ReactToolCallRenderer<unknown>["render"];
161
163
  const isExecuting = executingToolCallIds.has(toolCall.id);
162
164
 
163
165
  // Use the memoized ToolCallRenderer component to prevent unnecessary re-renders
@@ -176,3 +178,31 @@ export function useRenderToolCall() {
176
178
 
177
179
  return renderToolCall;
178
180
  }
181
+
182
+ // Adapter that bridges the ReactToolCallRenderer signature
183
+ // (`{ name, args, status, result, toolCallId }`) to the
184
+ // `DefaultToolCallRenderer` signature (`{ name, parameters, status,
185
+ // result }`) so the latter can be used as a zero-config fallback when
186
+ // no `*` renderer is registered.
187
+ function defaultToolCallRenderAdapter(props: {
188
+ name: string;
189
+ args: unknown;
190
+ status: ToolCallStatus;
191
+ result: string | undefined;
192
+ toolCallId: string;
193
+ }): React.ReactElement {
194
+ const status =
195
+ props.status === ToolCallStatus.Complete
196
+ ? "complete"
197
+ : props.status === ToolCallStatus.Executing
198
+ ? "executing"
199
+ : "inProgress";
200
+ return (
201
+ <DefaultToolCallRenderer
202
+ name={props.name}
203
+ parameters={props.args}
204
+ status={status}
205
+ result={props.result}
206
+ />
207
+ );
208
+ }