Adding a button for copying error messages (#1882)
close #1870 <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Adds a “Copy” button to error banners and chat error output so users can quickly copy error messages with clear feedback. Addresses Linear #1870. - **New Features** - Introduced CopyErrorMessage component that writes to clipboard and shows “Copied” for 2s. - Added the copy button to the Preview error banner and DyadOutput; actions grouped at the bottom beside “Fix with AI”. - Added Playwright e2e test and helpers to verify copy behavior and clipboard content. <sup>Written for commit 12e9bf1437ded36dc022e1d795025580d2ffd111. Summary will update automatically on new commits.</sup> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
committed by
GitHub
parent
4b17870049
commit
c174778d5f
@@ -1,4 +1,5 @@
|
||||
import { testSkipIfWindows, test } from "./helpers/test_helper";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
testSkipIfWindows("fix error with AI", async ({ po }) => {
|
||||
await po.setUp({ autoApprove: true });
|
||||
@@ -20,6 +21,26 @@ testSkipIfWindows("fix error with AI", async ({ po }) => {
|
||||
await po.snapshotPreview();
|
||||
});
|
||||
|
||||
testSkipIfWindows("copy error message from banner", async ({ po }) => {
|
||||
await po.setUp({ autoApprove: true });
|
||||
await po.sendPrompt("tc=create-error");
|
||||
|
||||
await po.page.getByText("Error Line 6 error", { exact: true }).waitFor({
|
||||
state: "visible",
|
||||
});
|
||||
|
||||
await po.clickCopyErrorMessage();
|
||||
|
||||
const clipboardText = await po.getClipboardText();
|
||||
expect(clipboardText).toContain("Error Line 6 error");
|
||||
expect(clipboardText.length).toBeGreaterThan(0);
|
||||
|
||||
await expect(po.page.getByRole("button", { name: "Copied" })).toBeVisible();
|
||||
|
||||
await expect(po.page.getByRole("button", { name: "Copied" })).toBeHidden({
|
||||
timeout: 3000,
|
||||
});
|
||||
});
|
||||
test("fix all errors button", async ({ po }) => {
|
||||
await po.setUp({ autoApprove: true });
|
||||
await po.sendPrompt("tc=create-multiple-errors");
|
||||
|
||||
@@ -575,6 +575,13 @@ export class PageObject {
|
||||
await this.page.getByRole("button", { name: "Fix error with AI" }).click();
|
||||
}
|
||||
|
||||
async clickCopyErrorMessage() {
|
||||
await this.page.getByRole("button", { name: /Copy/ }).click();
|
||||
}
|
||||
|
||||
async getClipboardText(): Promise<string> {
|
||||
return await this.page.evaluate(() => navigator.clipboard.readText());
|
||||
}
|
||||
async clickFixAllErrors() {
|
||||
await this.page.getByRole("button", { name: /Fix All Errors/ }).click();
|
||||
}
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
- img
|
||||
- text: "src/pages/Index.tsx Summary: intentionally add first error"
|
||||
- img
|
||||
- text: Error
|
||||
- button "Fix with AI":
|
||||
- text: Error First error in Index...
|
||||
- img
|
||||
- text: First error in Index...
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Fix with AI":
|
||||
- img
|
||||
- img
|
||||
- text: ErrorComponent.tsx
|
||||
@@ -19,10 +20,11 @@
|
||||
- img
|
||||
- text: "src/components/ErrorComponent.tsx Summary: intentionally add second error"
|
||||
- img
|
||||
- text: Error
|
||||
- button "Fix with AI":
|
||||
- text: Error Second error in ErrorComponent...
|
||||
- img
|
||||
- text: Second error in ErrorComponent...
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Fix with AI":
|
||||
- img
|
||||
- img
|
||||
- text: helper.ts
|
||||
@@ -31,10 +33,11 @@
|
||||
- img
|
||||
- text: "src/utils/helper.ts Summary: intentionally add third error"
|
||||
- img
|
||||
- text: Error
|
||||
- button "Fix with AI":
|
||||
- text: Error Third error in helper...
|
||||
- img
|
||||
- text: Third error in helper...
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Fix with AI":
|
||||
- img
|
||||
- button "Fix All Errors (3)":
|
||||
- img
|
||||
|
||||
49
src/components/CopyErrorMessage.tsx
Normal file
49
src/components/CopyErrorMessage.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Copy, Check } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface CopyErrorMessageProps {
|
||||
errorMessage: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const CopyErrorMessage = ({
|
||||
errorMessage,
|
||||
className = "",
|
||||
}: CopyErrorMessageProps) => {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const handleCopy = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
await navigator.clipboard.writeText(errorMessage);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy error message:", err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${
|
||||
isCopied
|
||||
? "bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300"
|
||||
: "bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
} ${className}`}
|
||||
title={isCopied ? "Copied!" : "Copy error message"}
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<Check size={14} />
|
||||
<span>Copied</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy size={14} />
|
||||
<span>Copy</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { useAtomValue } from "jotai";
|
||||
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
|
||||
import { useStreamChat } from "@/hooks/useStreamChat";
|
||||
import { CopyErrorMessage } from "@/components/CopyErrorMessage";
|
||||
interface DyadOutputProps {
|
||||
type: "error" | "warning";
|
||||
message?: string;
|
||||
@@ -59,19 +60,6 @@ export const DyadOutput: React.FC<DyadOutputProps> = ({
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
|
||||
{/* Fix with AI button - always visible for errors */}
|
||||
{isError && message && (
|
||||
<div className="absolute top-9 left-2">
|
||||
<button
|
||||
onClick={handleAIFix}
|
||||
className="cursor-pointer flex items-center justify-center bg-red-600 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800 text-white rounded text-xs p-1 w-24 h-6"
|
||||
>
|
||||
<Sparkles size={16} className="mr-1" />
|
||||
<span>Fix with AI</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main content, padded to avoid label */}
|
||||
<div className="flex items-center justify-between pl-24 pr-6">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -103,6 +91,22 @@ export const DyadOutput: React.FC<DyadOutputProps> = ({
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action buttons at the bottom - always visible for errors */}
|
||||
{isError && message && (
|
||||
<div className="mt-3 px-6 flex justify-end gap-2">
|
||||
<CopyErrorMessage
|
||||
errorMessage={children ? `${message}\n${children}` : message}
|
||||
/>
|
||||
<button
|
||||
onClick={handleAIFix}
|
||||
className="cursor-pointer flex items-center justify-center bg-red-600 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800 text-white rounded text-xs px-2 py-1 h-6"
|
||||
>
|
||||
<Sparkles size={14} className="mr-1" />
|
||||
<span>Fix with AI</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
Smartphone,
|
||||
} from "lucide-react";
|
||||
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
|
||||
import { CopyErrorMessage } from "@/components/CopyErrorMessage";
|
||||
import { IpcClient } from "@/ipc/ipc_client";
|
||||
|
||||
import { useParseRouter } from "@/hooks/useParseRouter";
|
||||
@@ -136,13 +137,14 @@ const ErrorBanner = ({ error, onDismiss, onAIFix }: ErrorBannerProps) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Fix button at the bottom */}
|
||||
{/* Action buttons at the bottom */}
|
||||
{!isDockerError && error.source === "preview-app" && (
|
||||
<div className="mt-2 flex justify-end">
|
||||
<div className="mt-3 px-6 flex justify-end gap-2">
|
||||
<CopyErrorMessage errorMessage={error.message} />
|
||||
<button
|
||||
disabled={isStreaming}
|
||||
onClick={onAIFix}
|
||||
className="cursor-pointer flex items-center space-x-1 px-2 py-0.5 bg-red-500 dark:bg-red-600 text-white rounded text-sm hover:bg-red-600 dark:hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="cursor-pointer flex items-center space-x-1 px-2 py-1 bg-red-500 dark:bg-red-600 text-white rounded text-sm hover:bg-red-600 dark:hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Sparkles size={14} />
|
||||
<span>Fix error with AI</span>
|
||||
|
||||
Reference in New Issue
Block a user