Skip to content

Commit 53b7fa2

Browse files
marthakellyclaude
andcommitted
chore: merge main into fix/CPK-7154-agent-text-wiped-multiple-tool-calls
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2 parents ccfff2d + e748a3d commit 53b7fa2

191 files changed

Lines changed: 19527 additions & 1952 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@copilotkitnext/angular": patch
3+
"@copilotkit/core": patch
4+
"@copilotkit/react-core": patch
5+
---
6+
7+
Add auto-detection of runtime transport (REST vs single-endpoint)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@copilotkit/react-core": patch
3+
---
4+
5+
fix: stabilize messageView and labels props to prevent message list re-renders on every keystroke
6+
7+
Passing `messageView` or `labels` as inline object props to `<CopilotChat />` previously caused all completed assistant messages to re-render on every keystroke due to reference instability. This was especially severe with large message histories (DocuSign: 100+ messages reported 2s→16s send time degradation).
8+
9+
Root causes fixed:
10+
11+
- `ts-deepmerge.merge()` deep-cloned plain objects even from a single source, creating a new reference every render that defeated `MemoizedSlotWrapper`'s shallow equality check. Replaced with shallow spread + `useShallowStableRef`.
12+
- Inline `labels` objects created a new `mergedLabels` context value every render, causing all `useCopilotChatConfiguration()` consumers across every message to re-render. Fixed by stabilizing with `useShallowStableRef` in `CopilotChatConfigurationProvider`.
13+
14+
The `useShallowStableRef` hook (added to `slots.tsx`) is now the single stabilization primitive: it returns the same reference as long as the value is shallowly equal, with an `isPlainObject` guard to avoid incorrect equality for arrays, Dates, and class instances.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@copilotkit/runtime": minor
3+
"@copilotkit/react-core": minor
4+
"@copilotkit/react-ui": minor
5+
"@copilotkit/shared": minor
6+
---
7+
8+
feat: add multimodal attachment support to the builtin agent

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
22
.vscode
3+
.chalk
34
.claude/*.local.json
45
.claude/worktrees
56

@@ -59,3 +60,4 @@ lefthook-local.yml
5960
*.a
6061
*.lib
6162
*.wasm
63+
tasks/
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
import { describe, it, expect } from "vitest";
2+
import jscodeshift from "jscodeshift";
3+
import transform from "../migrate-attachments";
4+
5+
const j = jscodeshift.withParser("tsx");
6+
7+
function run(source: string): string {
8+
const result = transform(
9+
{ source, path: "test.tsx" },
10+
{ jscodeshift: j, j, stats: () => {}, report: () => {} },
11+
);
12+
return result ?? source;
13+
}
14+
15+
describe("migrate-attachments codemod", () => {
16+
// -----------------------------------------------------------------------
17+
// Props transformation
18+
// -----------------------------------------------------------------------
19+
20+
describe("JSX props", () => {
21+
it("transforms imageUploadsEnabled to attachments", () => {
22+
const input = `
23+
import { CopilotChat } from "@copilotkit/react-ui";
24+
<CopilotChat imageUploadsEnabled={true} />;
25+
`;
26+
const output = run(input);
27+
expect(output).toContain("attachments={{");
28+
expect(output).toContain("enabled: true");
29+
expect(output).not.toContain("imageUploadsEnabled");
30+
});
31+
32+
it("transforms inputFileAccept to attachments.accept", () => {
33+
const input = `
34+
import { CopilotChat } from "@copilotkit/react-ui";
35+
<CopilotChat inputFileAccept="image/*" />;
36+
`;
37+
const output = run(input);
38+
expect(output).toContain('accept: "image/*"');
39+
expect(output).not.toContain("inputFileAccept");
40+
});
41+
42+
it("merges both props into a single attachments object", () => {
43+
const input = `
44+
import { CopilotChat } from "@copilotkit/react-ui";
45+
<CopilotChat imageUploadsEnabled={true} inputFileAccept="image/*,.pdf" />;
46+
`;
47+
const output = run(input);
48+
expect(output).toContain("enabled: true");
49+
expect(output).toContain('accept: "image/*,.pdf"');
50+
expect(output).not.toContain("imageUploadsEnabled");
51+
expect(output).not.toContain("inputFileAccept");
52+
});
53+
54+
it("works on CopilotSidebar", () => {
55+
const input = `
56+
import { CopilotSidebar } from "@copilotkit/react-ui";
57+
<CopilotSidebar imageUploadsEnabled={true} />;
58+
`;
59+
const output = run(input);
60+
expect(output).toContain("attachments={{");
61+
expect(output).not.toContain("imageUploadsEnabled");
62+
});
63+
64+
it("works on CopilotPopup", () => {
65+
const input = `
66+
import { CopilotPopup } from "@copilotkit/react-ui";
67+
<CopilotPopup imageUploadsEnabled={true} />;
68+
`;
69+
const output = run(input);
70+
expect(output).toContain("attachments={{");
71+
expect(output).not.toContain("imageUploadsEnabled");
72+
});
73+
74+
it("preserves other props", () => {
75+
const input = `
76+
import { CopilotChat } from "@copilotkit/react-ui";
77+
<CopilotChat className="my-chat" imageUploadsEnabled={true} labels={{ placeholder: "Ask..." }} />;
78+
`;
79+
const output = run(input);
80+
expect(output).toContain('className="my-chat"');
81+
expect(output).toContain("labels=");
82+
expect(output).toContain("attachments={{");
83+
});
84+
85+
it("preserves imageUploadsEnabled={false}", () => {
86+
const input = `
87+
import { CopilotChat } from "@copilotkit/react-ui";
88+
<CopilotChat imageUploadsEnabled={false} />;
89+
`;
90+
const output = run(input);
91+
expect(output).toContain("enabled: false");
92+
expect(output).not.toContain("enabled: true");
93+
});
94+
95+
it("preserves dynamic imageUploadsEnabled expression", () => {
96+
const input = `
97+
import { CopilotChat } from "@copilotkit/react-ui";
98+
<CopilotChat imageUploadsEnabled={isEnabled} />;
99+
`;
100+
const output = run(input);
101+
expect(output).toContain("enabled: isEnabled");
102+
});
103+
104+
it("handles shorthand imageUploadsEnabled (no value)", () => {
105+
const input = `
106+
import { CopilotChat } from "@copilotkit/react-ui";
107+
<CopilotChat imageUploadsEnabled />;
108+
`;
109+
const output = run(input);
110+
expect(output).toContain("enabled: true");
111+
});
112+
113+
it("skips if attachments prop already exists", () => {
114+
const input = `
115+
import { CopilotChat } from "@copilotkit/react-ui";
116+
<CopilotChat imageUploadsEnabled={true} attachments={{ enabled: true }} />;
117+
`;
118+
const output = run(input);
119+
// Should not double-add attachments — leaves as-is
120+
expect(output).toContain("imageUploadsEnabled");
121+
});
122+
123+
it("preserves dynamic inputFileAccept expression", () => {
124+
const input = `
125+
import { CopilotChat } from "@copilotkit/react-ui";
126+
<CopilotChat imageUploadsEnabled={true} inputFileAccept={acceptTypes} />;
127+
`;
128+
const output = run(input);
129+
expect(output).toContain("accept: acceptTypes");
130+
expect(output).toContain("enabled: true");
131+
expect(output).not.toContain("inputFileAccept");
132+
});
133+
134+
it("ignores non-CopilotKit components", () => {
135+
const input = `
136+
<SomeOtherChat imageUploadsEnabled={true} />;
137+
`;
138+
const output = run(input);
139+
expect(output).toContain("imageUploadsEnabled");
140+
});
141+
});
142+
143+
// -----------------------------------------------------------------------
144+
// Import renames
145+
// -----------------------------------------------------------------------
146+
147+
describe("import renames", () => {
148+
it("renames ImageUploadQueue to AttachmentQueue", () => {
149+
const input = `
150+
import { ImageUploadQueue } from "@copilotkit/react-ui";
151+
<ImageUploadQueue images={imgs} />;
152+
`;
153+
const output = run(input);
154+
expect(output).toContain("AttachmentQueue");
155+
expect(output).not.toContain("ImageUploadQueue");
156+
});
157+
158+
it("renames ImageUpload type to Attachment", () => {
159+
const input = `
160+
import type { ImageUpload } from "@copilotkit/react-ui";
161+
const x: ImageUpload = { contentType: "", bytes: "" };
162+
`;
163+
const output = run(input);
164+
expect(output).toContain("Attachment");
165+
expect(output).not.toContain("ImageUpload");
166+
});
167+
168+
it("handles aliased imports (keeps alias, renames imported)", () => {
169+
const input = `
170+
import { ImageUploadQueue as MyQueue } from "@copilotkit/react-ui";
171+
<MyQueue images={imgs} />;
172+
`;
173+
const output = run(input);
174+
// Imported name changes, but local alias stays
175+
expect(output).toContain("AttachmentQueue as MyQueue");
176+
expect(output).toContain("<MyQueue");
177+
});
178+
179+
it("ignores imports from other packages", () => {
180+
const input = `
181+
import { ImageUploadQueue } from "some-other-package";
182+
`;
183+
const output = run(input);
184+
expect(output).toContain("ImageUploadQueue");
185+
expect(output).not.toContain("AttachmentQueue");
186+
});
187+
188+
it("does not rename local variables that shadow the import name", () => {
189+
const input = `
190+
import type { ImageUpload } from "@copilotkit/react-ui";
191+
const x: ImageUpload = {} as any;
192+
const ImageUpload = "unrelated local variable";
193+
console.log(ImageUpload);
194+
`;
195+
const output = run(input);
196+
// Import and type reference should be renamed
197+
expect(output).toContain("import type { Attachment }");
198+
expect(output).toContain("const x: Attachment");
199+
// Local variable declaration and its reference should NOT be renamed
200+
expect(output).toContain(
201+
'const ImageUpload = "unrelated local variable"',
202+
);
203+
expect(output).toContain("console.log(ImageUpload)");
204+
});
205+
206+
it("does not rename object property keys or member expressions", () => {
207+
const input = `
208+
import type { ImageUpload } from "@copilotkit/react-ui";
209+
const x: ImageUpload = {} as any;
210+
const config = { ImageUpload: true };
211+
const val = obj.ImageUpload;
212+
`;
213+
const output = run(input);
214+
// Import and type reference should be renamed
215+
expect(output).toContain("import type { Attachment }");
216+
expect(output).toContain("const x: Attachment");
217+
// Object key and member access should NOT be renamed
218+
expect(output).toContain("{ ImageUpload: true }");
219+
expect(output).toContain("obj.ImageUpload");
220+
});
221+
});
222+
223+
// -----------------------------------------------------------------------
224+
// Idempotency and no-op
225+
// -----------------------------------------------------------------------
226+
227+
describe("idempotency", () => {
228+
it("running twice produces the same result", () => {
229+
const input = `
230+
import { CopilotChat, ImageUploadQueue } from "@copilotkit/react-ui";
231+
<CopilotChat imageUploadsEnabled={true} inputFileAccept="image/*" />;
232+
<ImageUploadQueue images={imgs} />;
233+
`;
234+
const first = run(input);
235+
const second = run(first);
236+
expect(second).toBe(first);
237+
});
238+
});
239+
240+
describe("no-op on unrelated code", () => {
241+
it("does not modify files without deprecated APIs", () => {
242+
const input = `
243+
import { useState } from "react";
244+
function App() {
245+
const [count, setCount] = useState(0);
246+
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
247+
}
248+
`;
249+
const output = run(input);
250+
expect(output).toBe(input);
251+
});
252+
253+
it("does not modify already-migrated code", () => {
254+
const input = `
255+
import { CopilotChat, AttachmentQueue } from "@copilotkit/react-ui";
256+
import type { Attachment } from "@copilotkit/react-ui";
257+
<CopilotChat attachments={{ enabled: true, accept: "image/*" }} />;
258+
`;
259+
const output = run(input);
260+
expect(output).toBe(input);
261+
});
262+
});
263+
});

0 commit comments

Comments
 (0)