@copilotkit/react-core 1.57.1 → 1.57.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.
- package/dist/{copilotkit-sQWiKtxA.d.cts → copilotkit-BK9CVq9A.d.cts} +6 -1
- package/dist/{copilotkit-sQWiKtxA.d.cts.map → copilotkit-BK9CVq9A.d.cts.map} +1 -1
- package/dist/{copilotkit-DjxXMYHG.mjs → copilotkit-CC8DjOiC.mjs} +404 -367
- package/dist/copilotkit-CC8DjOiC.mjs.map +1 -0
- package/dist/{copilotkit-C3k13WZn.cjs → copilotkit-CtXcs1ea.cjs} +403 -366
- package/dist/copilotkit-CtXcs1ea.cjs.map +1 -0
- package/dist/{copilotkit-BN4I_y1n.d.mts → copilotkit-WlmeVijs.d.mts} +6 -1
- package/dist/{copilotkit-BN4I_y1n.d.mts.map → copilotkit-WlmeVijs.d.mts.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.umd.js +191 -15
- package/dist/index.umd.js.map +1 -1
- package/dist/v2/headless.cjs +110 -3
- package/dist/v2/headless.cjs.map +1 -1
- package/dist/v2/headless.d.cts +142 -2
- package/dist/v2/headless.d.cts.map +1 -1
- package/dist/v2/headless.d.mts +142 -1
- package/dist/v2/headless.d.mts.map +1 -1
- package/dist/v2/headless.mjs +108 -4
- package/dist/v2/headless.mjs.map +1 -1
- package/dist/v2/index.cjs +1 -1
- package/dist/v2/index.css +1 -1
- package/dist/v2/index.d.cts +1 -1
- package/dist/v2/index.d.mts +1 -1
- package/dist/v2/index.mjs +1 -1
- package/dist/v2/index.umd.js +403 -364
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +6 -6
- package/src/v2/components/chat/CopilotSidebar.tsx +5 -1
- package/src/v2/components/chat/CopilotSidebarView.tsx +24 -10
- package/src/v2/components/chat/__tests__/CopilotChatPerf.e2e.test.tsx +28 -0
- package/src/v2/components/chat/__tests__/CopilotSidebarView.position.test.tsx +159 -0
- package/src/v2/headless.ts +23 -1
- package/src/v2/hooks/__tests__/use-component.test.tsx +4 -1
- package/src/v2/hooks/use-component.tsx +2 -0
- package/src/v2/hooks/use-default-render-tool.tsx +18 -1
- package/src/v2/hooks/use-render-tool-call.tsx +35 -5
- package/dist/copilotkit-C3k13WZn.cjs.map +0 -1
- 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.
|
|
3
|
+
"version": "1.57.2",
|
|
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/
|
|
85
|
-
"@copilotkit/
|
|
86
|
-
"@copilotkit/web-inspector": "1.57.
|
|
87
|
-
"@copilotkit/runtime-client-gql": "1.57.
|
|
88
|
-
"@copilotkit/
|
|
84
|
+
"@copilotkit/core": "1.57.2",
|
|
85
|
+
"@copilotkit/shared": "1.57.2",
|
|
86
|
+
"@copilotkit/web-inspector": "1.57.2",
|
|
87
|
+
"@copilotkit/runtime-client-gql": "1.57.2",
|
|
88
|
+
"@copilotkit/a2ui-renderer": "1.57.2"
|
|
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 =
|
|
132
|
+
document.body.style.transition = `${transitionCssProp} ${SIDEBAR_TRANSITION_MS}ms ease`;
|
|
124
133
|
}
|
|
125
|
-
document.body.style
|
|
134
|
+
document.body.style[marginStyleProp] = widthToMargin(sidebarWidth);
|
|
126
135
|
} else if (hasMounted.current) {
|
|
127
|
-
document.body.style.transition =
|
|
128
|
-
document.body.style
|
|
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
|
|
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:
|
|
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
|
-
"
|
|
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
|
-
:
|
|
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,34 @@ describe("CopilotChat perf — re-render regression", () => {
|
|
|
115
115
|
renderCounts.clear();
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
+
// TanStack Virtual schedules rAF callbacks for measurement. On Node 20 the
|
|
119
|
+
// jsdom timing is different — draining a fixed number of rAF rounds is not
|
|
120
|
+
// enough because the observer keeps scheduling new callbacks. When
|
|
121
|
+
// @testing-library/react's auto-cleanup unmounts the component AFTER our
|
|
122
|
+
// afterEach, any pending rAF fires against a torn-down window (null) and
|
|
123
|
+
// throws: TypeError: Cannot read properties of null ('requestAnimationFrame').
|
|
124
|
+
//
|
|
125
|
+
// Fix: after draining what we can, replace rAF with a no-op so any callback
|
|
126
|
+
// scheduled during (or after) RTL cleanup is silently swallowed. Restore the
|
|
127
|
+
// real rAF at the start of the next test via beforeEach.
|
|
128
|
+
const realRAF = globalThis.requestAnimationFrame;
|
|
129
|
+
|
|
130
|
+
afterEach(async () => {
|
|
131
|
+
// Best-effort drain of already-queued rAF callbacks.
|
|
132
|
+
await act(async () => {
|
|
133
|
+
await new Promise<void>((r) => realRAF(() => r()));
|
|
134
|
+
await new Promise<void>((r) => realRAF(() => r()));
|
|
135
|
+
});
|
|
136
|
+
// Neuter rAF so callbacks scheduled during RTL cleanup are harmless.
|
|
137
|
+
globalThis.requestAnimationFrame = ((_cb: FrameRequestCallback) =>
|
|
138
|
+
0) as typeof requestAnimationFrame;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
beforeEach(() => {
|
|
142
|
+
// Restore real rAF for the upcoming test.
|
|
143
|
+
globalThis.requestAnimationFrame = realRAF;
|
|
144
|
+
});
|
|
145
|
+
|
|
118
146
|
it("completed messages do not re-render when a new message is added", async () => {
|
|
119
147
|
const agent = new MockStepwiseAgent();
|
|
120
148
|
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
|
+
});
|
package/src/v2/headless.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const RenderComponent = renderConfig
|
|
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
|
+
}
|