Skip to content

Commit 1e0de53

Browse files
authored
fix: add null check for navigator.clipboard across all copy-to-clipboard calls (CopilotKit#2114) (CopilotKit#3813)
Fixes CopilotKit#2114 Red-green tested locally.
2 parents 5aa158d + fbf1af2 commit 1e0de53

13 files changed

Lines changed: 433 additions & 95 deletions

File tree

packages/angular/src/lib/components/chat/copilot-chat-assistant-message-buttons.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import { CopilotTooltip } from "../../directives/tooltip";
2121
import { cn } from "../../utils";
2222
import { injectChatLabels } from "../../chat-config";
23+
import { copyToClipboard } from "@copilotkit/shared";
2324

2425
// Base toolbar button component
2526
@Component({
@@ -121,18 +122,13 @@ export class CopilotChatAssistantMessageCopyButton {
121122
event?.stopPropagation();
122123
if (!this.content()) return;
123124

124-
// Set copied immediately for instant feedback
125-
this.copied.set(true);
126-
setTimeout(() => this.copied.set(false), 2000);
127-
128-
// Copy to clipboard (fire and forget)
129-
navigator.clipboard.writeText(this.content()!).then(
130-
() => this.clicked.emit(),
131-
(err) => {
132-
console.error("Failed to copy message:", err);
133-
this.copied.set(false);
134-
},
135-
);
125+
copyToClipboard(this.content()!).then((success) => {
126+
if (success) {
127+
this.copied.set(true);
128+
this.clicked.emit();
129+
setTimeout(() => this.copied.set(false), 2000);
130+
}
131+
});
136132
}
137133
}
138134

packages/angular/src/lib/components/chat/copilot-chat-assistant-message-renderer.ts

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import hljs from "highlight.js";
1616
import * as katex from "katex";
1717
import { completePartialMarkdown } from "@copilotkit/core";
1818
import { LucideAngularModule } from "lucide-angular";
19+
import { copyToClipboard } from "@copilotkit/shared";
1920
import { injectChatLabels } from "../../chat-config";
2021

2122
@Component({
@@ -387,37 +388,34 @@ export class CopilotChatAssistantMessageRenderer implements AfterViewInit {
387388
}
388389

389390
private copyCodeBlock(blockId: string, code: string): void {
390-
navigator.clipboard.writeText(code).then(
391-
() => {
392-
// Update the button in the DOM
393-
const button = this.elementRef.nativeElement.querySelector(
394-
`[data-code-block-id="${blockId}"]`,
395-
);
396-
if (button) {
397-
const originalHTML = button.innerHTML;
398-
button.innerHTML = `
391+
copyToClipboard(code).then((success) => {
392+
if (!success) return;
393+
394+
// Update the button in the DOM
395+
const button = this.elementRef.nativeElement.querySelector(
396+
`[data-code-block-id="${blockId}"]`,
397+
);
398+
if (button) {
399+
const originalHTML = button.innerHTML;
400+
button.innerHTML = `
399401
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
400402
<span>${this.labels.assistantMessageToolbarCopyCodeCopiedLabel}</span>
401403
`;
404+
button.setAttribute(
405+
"aria-label",
406+
`${this.labels.assistantMessageToolbarCopyCodeCopiedLabel} code`,
407+
);
408+
409+
// Reset after 2 seconds
410+
setTimeout(() => {
411+
button.innerHTML = originalHTML;
402412
button.setAttribute(
403413
"aria-label",
404-
`${this.labels.assistantMessageToolbarCopyCodeCopiedLabel} code`,
414+
`${this.labels.assistantMessageToolbarCopyCodeLabel} code`,
405415
);
406-
407-
// Reset after 2 seconds
408-
setTimeout(() => {
409-
button.innerHTML = originalHTML;
410-
button.setAttribute(
411-
"aria-label",
412-
`${this.labels.assistantMessageToolbarCopyCodeLabel} code`,
413-
);
414-
}, 2000);
415-
}
416-
},
417-
(err) => {
418-
console.error("Failed to copy code:", err);
419-
},
420-
);
416+
}, 2000);
417+
}
418+
});
421419
}
422420

423421
private generateBlockId(code: string): string {

packages/angular/src/lib/components/chat/copilot-chat-user-message-buttons.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { LucideAngularModule, Copy, Check, Edit } from "lucide-angular";
1212
import { CopilotTooltip } from "../../directives/tooltip";
1313
import { cn } from "../../utils";
1414
import { injectChatLabels } from "../../chat-config";
15+
import { copyToClipboard } from "@copilotkit/shared";
1516

1617
// Base toolbar button component
1718
@Component({
@@ -108,18 +109,13 @@ export class CopilotChatUserMessageCopyButton {
108109
handleCopy(): void {
109110
if (!this.content()) return;
110111

111-
// Set copied immediately for instant feedback
112-
this.copied.set(true);
113-
setTimeout(() => this.copied.set(false), 2000);
114-
115-
// Copy to clipboard (fire and forget)
116-
navigator.clipboard.writeText(this.content()!).then(
117-
() => this.clicked.emit(),
118-
(err) => {
119-
console.error("Failed to copy message:", err);
120-
this.copied.set(false);
121-
},
122-
);
112+
copyToClipboard(this.content()!).then((success) => {
113+
if (success) {
114+
this.copied.set(true);
115+
this.clicked.emit();
116+
setTimeout(() => this.copied.set(false), 2000);
117+
}
118+
});
123119
}
124120
}
125121

packages/react-core/src/v2/components/chat/CopilotChatAssistantMessage.tsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import { useKatexStyles } from "../../hooks/useKatexStyles";
2323
import { WithSlots, renderSlot } from "../../lib/slots";
2424
import { Streamdown } from "streamdown";
25+
import { copyToClipboard } from "@copilotkit/shared";
2526
import CopilotChatToolCallsView from "./CopilotChatToolCallsView";
2627

2728
export type CopilotChatAssistantMessageProps = WithSlots<
@@ -86,12 +87,9 @@ export function CopilotChatAssistantMessage({
8687
{
8788
onClick: async () => {
8889
if (message.content) {
89-
try {
90-
await navigator.clipboard.writeText(message.content);
91-
} catch (err) {
92-
console.error("Failed to copy message:", err);
93-
}
90+
return await copyToClipboard(message.content);
9491
}
92+
return false;
9593
},
9694
},
9795
);
@@ -275,18 +273,23 @@ export namespace CopilotChatAssistantMessage {
275273
};
276274
}, []);
277275

278-
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
279-
setCopied(true);
280-
if (timerRef.current !== null) {
281-
clearTimeout(timerRef.current);
276+
const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
277+
let success = false;
278+
if (onClick) {
279+
// onClick may return a boolean indicating copy success
280+
const result = await Promise.resolve(onClick(event));
281+
success = result === true;
282282
}
283-
timerRef.current = setTimeout(() => {
284-
timerRef.current = null;
285-
setCopied(false);
286-
}, 2000);
287283

288-
if (onClick) {
289-
onClick(event);
284+
if (success) {
285+
setCopied(true);
286+
if (timerRef.current !== null) {
287+
clearTimeout(timerRef.current);
288+
}
289+
timerRef.current = setTimeout(() => {
290+
timerRef.current = null;
291+
setCopied(false);
292+
}, 2000);
290293
}
291294
};
292295

packages/react-core/src/v2/components/chat/CopilotChatUserMessage.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
type AudioInputPart,
1919
type VideoInputPart,
2020
type DocumentInputPart,
21+
copyToClipboard,
2122
} from "@copilotkit/shared";
2223
import { CopilotChatAttachmentRenderer } from "./CopilotChatAttachmentRenderer";
2324

@@ -147,12 +148,9 @@ export function CopilotChatUserMessage({
147148
{
148149
onClick: async () => {
149150
if (flattenedContent) {
150-
try {
151-
await navigator.clipboard.writeText(flattenedContent);
152-
} catch (err) {
153-
console.error("Failed to copy message:", err);
154-
}
151+
return await copyToClipboard(flattenedContent);
155152
}
153+
return false;
156154
},
157155
},
158156
);
@@ -314,12 +312,17 @@ export namespace CopilotChatUserMessage {
314312
const labels = config?.labels ?? CopilotChatDefaultLabels;
315313
const [copied, setCopied] = useState(false);
316314

317-
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
318-
setCopied(true);
319-
setTimeout(() => setCopied(false), 2000);
320-
315+
const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
316+
let success = false;
321317
if (onClick) {
322-
onClick(event);
318+
// onClick may return a boolean indicating copy success
319+
const result = await Promise.resolve(onClick(event));
320+
success = result === true;
321+
}
322+
323+
if (success) {
324+
setCopied(true);
325+
setTimeout(() => setCopied(false), 2000);
323326
}
324327
};
325328

packages/react-core/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -669,7 +669,7 @@ describe("CopilotChatAssistantMessage", () => {
669669

670670
await waitFor(() => {
671671
expect(consoleSpy).toHaveBeenCalledWith(
672-
"Failed to copy message:",
672+
"Failed to copy to clipboard:",
673673
expect.any(Error),
674674
);
675675
});

0 commit comments

Comments
 (0)