diff --git a/e2e-tests/fix_error.spec.ts b/e2e-tests/fix_error.spec.ts index 0155997..4463169 100644 --- a/e2e-tests/fix_error.spec.ts +++ b/e2e-tests/fix_error.spec.ts @@ -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"); diff --git a/e2e-tests/helpers/test_helper.ts b/e2e-tests/helpers/test_helper.ts index d9ab5a5..6abe9ba 100644 --- a/e2e-tests/helpers/test_helper.ts +++ b/e2e-tests/helpers/test_helper.ts @@ -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 { + return await this.page.evaluate(() => navigator.clipboard.readText()); + } async clickFixAllErrors() { await this.page.getByRole("button", { name: /Fix All Errors/ }).click(); } diff --git a/e2e-tests/snapshots/fix_error.spec.ts_fix-all-errors-button-1.aria.yml b/e2e-tests/snapshots/fix_error.spec.ts_fix-all-errors-button-1.aria.yml index d202b96..6fc8064 100644 --- a/e2e-tests/snapshots/fix_error.spec.ts_fix-all-errors-button-1.aria.yml +++ b/e2e-tests/snapshots/fix_error.spec.ts_fix-all-errors-button-1.aria.yml @@ -7,11 +7,12 @@ - img - text: "src/pages/Index.tsx Summary: intentionally add first error" - img -- text: Error +- text: Error First error in Index... +- img +- button "Copy": + - img - button "Fix with AI": - img -- text: First error in Index... -- img - img - text: ErrorComponent.tsx - button "Edit": @@ -19,11 +20,12 @@ - img - text: "src/components/ErrorComponent.tsx Summary: intentionally add second error" - img -- text: Error +- text: Error Second error in ErrorComponent... +- img +- button "Copy": + - img - button "Fix with AI": - img -- text: Second error in ErrorComponent... -- img - img - text: helper.ts - button "Edit": @@ -31,11 +33,12 @@ - img - text: "src/utils/helper.ts Summary: intentionally add third error" - img -- text: Error +- text: Error Third error in helper... +- img +- button "Copy": + - img - button "Fix with AI": - img -- text: Third error in helper... -- img - button "Fix All Errors (3)": - img - button: diff --git a/src/components/CopyErrorMessage.tsx b/src/components/CopyErrorMessage.tsx new file mode 100644 index 0000000..82981fc --- /dev/null +++ b/src/components/CopyErrorMessage.tsx @@ -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 ( + + ); +}; diff --git a/src/components/chat/DyadOutput.tsx b/src/components/chat/DyadOutput.tsx index 792cb49..a24fbfc 100644 --- a/src/components/chat/DyadOutput.tsx +++ b/src/components/chat/DyadOutput.tsx @@ -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 = ({ {label} - {/* Fix with AI button - always visible for errors */} - {isError && message && ( -
- -
- )} - {/* Main content, padded to avoid label */}
@@ -103,6 +91,22 @@ export const DyadOutput: React.FC = ({ {children}
)} + + {/* Action buttons at the bottom - always visible for errors */} + {isError && message && ( +
+ + +
+ )}
); }; diff --git a/src/components/preview_panel/PreviewIframe.tsx b/src/components/preview_panel/PreviewIframe.tsx index 593668e..f428783 100644 --- a/src/components/preview_panel/PreviewIframe.tsx +++ b/src/components/preview_panel/PreviewIframe.tsx @@ -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) => { - {/* AI Fix button at the bottom */} + {/* Action buttons at the bottom */} {!isDockerError && error.source === "preview-app" && ( -
+
+