From fc0dc21920dc6ef82604d89f8e80013d70fa0472 Mon Sep 17 00:00:00 2001 From: Will Chen <7344640+wwwillchen@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:57:20 -0700 Subject: [PATCH 1/4] Make Fix with AI error banner collapsible --- e2e-tests/fix_error.spec.ts | 7 + .../page-objects/components/PreviewPanel.ts | 12 ++ ...error.spec.ts_fix-error-with-AI-1.aria.yml | 9 +- ...error.spec.ts_fix-error-with-AI-2.aria.yml | 9 +- .../preview_panel/PreviewErrorBanner.test.tsx | 87 +++++++++ .../preview_panel/PreviewErrorBanner.tsx | 168 ++++++++++++++++++ .../preview_panel/PreviewIframe.tsx | 111 +----------- 7 files changed, 288 insertions(+), 115 deletions(-) create mode 100644 src/components/preview_panel/PreviewErrorBanner.test.tsx create mode 100644 src/components/preview_panel/PreviewErrorBanner.tsx diff --git a/e2e-tests/fix_error.spec.ts b/e2e-tests/fix_error.spec.ts index c361237800..f5ed1c1756 100644 --- a/e2e-tests/fix_error.spec.ts +++ b/e2e-tests/fix_error.spec.ts @@ -9,6 +9,13 @@ testSkipIfWindows("fix error with AI", async ({ po }) => { name: "fix-error-with-AI-1.aria.yml", }); + await po.previewPanel.collapsePreviewErrorBanner(); + await expect(po.previewPanel.locatePreviewErrorBanner()).toBeVisible(); + await expect( + po.page.getByRole("button", { name: "Fix error with AI" }), + ).toBeHidden(); + await po.previewPanel.expandPreviewErrorBanner(); + await expect( po.page.getByText("Error Line 6 error", { exact: true }), ).toBeVisible({ timeout: Timeout.MEDIUM }); diff --git a/e2e-tests/helpers/page-objects/components/PreviewPanel.ts b/e2e-tests/helpers/page-objects/components/PreviewPanel.ts index 89d1589b75..ed7f0afbc8 100644 --- a/e2e-tests/helpers/page-objects/components/PreviewPanel.ts +++ b/e2e-tests/helpers/page-objects/components/PreviewPanel.ts @@ -320,6 +320,18 @@ export class PreviewPanel { await this.page.getByRole("button", { name: "Fix error with AI" }).click(); } + async collapsePreviewErrorBanner() { + await this.page + .getByRole("button", { name: "Collapse error banner" }) + .click(); + } + + async expandPreviewErrorBanner() { + await this.page + .getByRole("button", { name: "Expand error banner" }) + .click(); + } + async clickCopyErrorMessage() { await this.page .getByTestId("preview-error-banner") diff --git a/e2e-tests/snapshots/fix_error.spec.ts_fix-error-with-AI-1.aria.yml b/e2e-tests/snapshots/fix_error.spec.ts_fix-error-with-AI-1.aria.yml index 28fecdd5c3..124d6da6b8 100644 --- a/e2e-tests/snapshots/fix_error.spec.ts_fix-error-with-AI-1.aria.yml +++ b/e2e-tests/snapshots/fix_error.spec.ts_fix-error-with-AI-1.aria.yml @@ -1,7 +1,10 @@ -- button: +- button "Dismiss error banner": - img -- img -- text: Error Line 6 error +- button "Collapse error banner" [expanded]: + - img +- 'button "Show full error message: Error Line 6 error"': + - img + - text: "" - img - text: "Tip: Check if restarting the app fixes the error." - button "Copy": diff --git a/e2e-tests/snapshots/fix_error.spec.ts_fix-error-with-AI-2.aria.yml b/e2e-tests/snapshots/fix_error.spec.ts_fix-error-with-AI-2.aria.yml index 1c18c53f53..75dec54b6e 100644 --- a/e2e-tests/snapshots/fix_error.spec.ts_fix-error-with-AI-2.aria.yml +++ b/e2e-tests/snapshots/fix_error.spec.ts_fix-error-with-AI-2.aria.yml @@ -1,7 +1,10 @@ -- button: +- button "Dismiss error banner": - img -- img -- text: "/Error Line 6 error Stack trace: Index \\(http:\\/\\/localhost:\\d+\\/src\\/pages\\/Index\\.tsx:6:6\\)/" +- button "Collapse error banner" [expanded]: + - img +- 'button /Hide full error message: Error Line 6 error Stack trace: Index \(http:\/\/localhost:\d+\/src\/pages\/Index\.tsx:6:6\)/ [expanded]': + - img + - text: "" - img - text: "Tip: Check if restarting the app fixes the error." - button "Copy": diff --git a/src/components/preview_panel/PreviewErrorBanner.test.tsx b/src/components/preview_panel/PreviewErrorBanner.test.tsx new file mode 100644 index 0000000000..9fb3ef0c00 --- /dev/null +++ b/src/components/preview_panel/PreviewErrorBanner.test.tsx @@ -0,0 +1,87 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { PreviewErrorBanner } from "./PreviewErrorBanner"; + +vi.mock("@/hooks/useStreamChat", () => ({ + useStreamChat: () => ({ isStreaming: false }), +})); + +vi.mock("@/components/CopyErrorMessage", () => ({ + CopyErrorMessage: () => , +})); + +const previewError = { + message: "Error Line 6 error\nStack trace: Index.tsx:6:6", + source: "preview-app" as const, +}; + +describe("PreviewErrorBanner", () => { + it("collapses to a compact summary and can be expanded again", () => { + render( + , + ); + + expect(screen.getByText(/Tip:/)).toBeTruthy(); + expect( + screen.getByRole("button", { name: "Fix error with AI" }), + ).toBeTruthy(); + + fireEvent.click( + screen.getByRole("button", { name: "Collapse error banner" }), + ); + + expect(screen.getByTestId("preview-error-banner")).toBeTruthy(); + expect(screen.getByText("Error Line 6 error")).toBeTruthy(); + expect(screen.queryByText(/Tip:/)).toBeNull(); + expect( + screen.queryByRole("button", { name: "Fix error with AI" }), + ).toBeNull(); + + fireEvent.click( + screen.getByRole("button", { name: "Expand error banner" }), + ); + + expect(screen.getByText(/Tip:/)).toBeTruthy(); + expect( + screen.getByRole("button", { name: "Fix error with AI" }), + ).toBeTruthy(); + }); + + it("keeps dismissal separate from collapsing", () => { + const onDismiss = vi.fn(); + render( + , + ); + + fireEvent.click( + screen.getByRole("button", { name: "Dismiss error banner" }), + ); + + expect(onDismiss).toHaveBeenCalledTimes(1); + }); + + it("still reveals the full error message independently", () => { + render( + , + ); + + expect(screen.queryByText(/Stack trace/)).toBeNull(); + fireEvent.click( + screen.getByRole("button", { name: /Show full error message/ }), + ); + + expect(screen.getByText(/Stack trace/)).toBeTruthy(); + }); +}); diff --git a/src/components/preview_panel/PreviewErrorBanner.tsx b/src/components/preview_panel/PreviewErrorBanner.tsx new file mode 100644 index 0000000000..799067befb --- /dev/null +++ b/src/components/preview_panel/PreviewErrorBanner.tsx @@ -0,0 +1,168 @@ +import { useState } from "react"; +import { ChevronRight, Lightbulb, Sparkles, X } from "lucide-react"; +import { CopyErrorMessage } from "@/components/CopyErrorMessage"; +import { useStreamChat } from "@/hooks/useStreamChat"; +import { cn } from "@/lib/utils"; + +interface PreviewErrorBannerProps { + error: + | { + message: string; + source: "preview-app" | "dyad-app" | "dyad-sync"; + } + | undefined; + onDismiss: () => void; + onAIFix: () => void; +} + +export function PreviewErrorBanner({ + error, + onDismiss, + onAIFix, +}: PreviewErrorBannerProps) { + const [isBannerCollapsed, setIsBannerCollapsed] = useState(false); + const [isErrorMessageCollapsed, setIsErrorMessageCollapsed] = useState(true); + const { isStreaming } = useStreamChat(); + + if (!error) return null; + + const isDockerError = error.message.includes("Cannot connect to the Docker"); + const isInternalDyadError = error.source === "dyad-app"; + const isSyncError = error.source === "dyad-sync"; + + const getTruncatedError = () => { + const firstLine = error.message.split("\n")[0]; + const snippetLength = 250; + const snippet = error.message.substring(0, snippetLength); + return firstLine.length < snippet.length + ? firstLine + : snippet + (snippet.length === snippetLength ? "..." : ""); + }; + + return ( +
+ + + + {(isInternalDyadError || isSyncError) && ( +
+ {isSyncError ? "Cloud sync issue" : "Internal Dyad error"} +
+ )} + + {isBannerCollapsed ? ( +
+ {getTruncatedError()} +
+ ) : ( +
+ {/* Error message in the middle */} +
+ +
+ + {/* Tip message */} +
+
+
+ +
+ + Tip: + {isDockerError + ? "Make sure Docker Desktop is running and try restarting the app." + : isSyncError + ? "Dyad could not upload your latest local changes to the cloud sandbox. Check your network connection or wait for sync to recover." + : isInternalDyadError + ? "Try restarting the Dyad app or restarting your computer to see if that fixes the error." + : "Check if restarting the app fixes the error."} + +
+
+ + {/* Action buttons at the bottom */} + {!isDockerError && error.source === "preview-app" && ( +
+ + +
+ )} +
+ )} +
+ ); +} diff --git a/src/components/preview_panel/PreviewIframe.tsx b/src/components/preview_panel/PreviewIframe.tsx index 19cfffe6f9..fdea6a190e 100644 --- a/src/components/preview_panel/PreviewIframe.tsx +++ b/src/components/preview_panel/PreviewIframe.tsx @@ -18,10 +18,7 @@ import { Cloud, Cog, X, - Sparkles, - Lightbulb, ChevronDown, - ChevronRight, MousePointerClick, Power, MonitorSmartphone, @@ -35,7 +32,6 @@ import { Loader2, } from "lucide-react"; import { selectedChatIdAtom } from "@/atoms/chatAtoms"; -import { CopyErrorMessage } from "@/components/CopyErrorMessage"; import { ipc } from "@/ipc/types"; import { useParseRouter } from "@/hooks/useParseRouter"; @@ -94,6 +90,7 @@ import { RecordingBannerHost } from "./RecordingBannerHost"; import { RecordingStorageWarningDialog } from "./RecordingStorageWarningDialog"; import { resolvePreviewBrowserUrl } from "./previewBrowserUrl"; import { PreviewLoadingScreen } from "./PreviewLoadingScreen"; +import { PreviewErrorBanner } from "./PreviewErrorBanner"; import { useTranslation } from "react-i18next"; import { formatPreviewAddressPath, @@ -114,110 +111,6 @@ import { } from "@/screenshot/useScreenshot"; import { useAppRunRemoteManager } from "@/app_run/AppRunRemoteProvider"; -interface ErrorBannerProps { - error: - | { - message: string; - source: "preview-app" | "dyad-app" | "dyad-sync"; - } - | undefined; - onDismiss: () => void; - onAIFix: () => void; -} - -const ErrorBanner = ({ error, onDismiss, onAIFix }: ErrorBannerProps) => { - const [isCollapsed, setIsCollapsed] = useState(true); - const { isStreaming } = useStreamChat(); - if (!error) return null; - const isDockerError = error.message.includes("Cannot connect to the Docker"); - const isInternalDyadError = error.source === "dyad-app"; - const isSyncError = error.source === "dyad-sync"; - - const getTruncatedError = () => { - const firstLine = error.message.split("\n")[0]; - const snippetLength = 250; - const snippet = error.message.substring(0, snippetLength); - return firstLine.length < snippet.length - ? firstLine - : snippet + (snippet.length === snippetLength ? "..." : ""); - }; - - return ( -
- {/* Close button in top left */} - - - {(isInternalDyadError || isSyncError) && ( -
- {isSyncError ? "Cloud sync issue" : "Internal Dyad error"} -
- )} - - {/* Error message in the middle */} -
-
setIsCollapsed(!isCollapsed)} - > - - - {isCollapsed ? getTruncatedError() : error.message} -
-
- - {/* Tip message */} -
-
-
- -
- - Tip: - {isDockerError - ? "Make sure Docker Desktop is running and try restarting the app." - : isSyncError - ? "Dyad could not upload your latest local changes to the cloud sandbox. Check your network connection or wait for sync to recover." - : isInternalDyadError - ? "Try restarting the Dyad app or restarting your computer to see if that fixes the error." - : "Check if restarting the app fixes the error."} - -
-
- - {/* Action buttons at the bottom */} - {!isDockerError && error.source === "preview-app" && ( -
- - -
- )} -
- ); -}; - // Preview iframe component export const PreviewIframe = ({ loading, @@ -1696,7 +1589,7 @@ export const PreviewIframe = ({
{!loading && ( - sendIframeEvent({ type: "DISMISS" })} onAIFix={() => { From 116aa55212eacae7b0fa35164076e8bdef04fcfc Mon Sep 17 00:00:00 2001 From: Will Chen <7344640+wwwillchen@users.noreply.github.com> Date: Tue, 25 Aug 2026 14:29:07 -0700 Subject: [PATCH 2/4] Polish collapsible error banner layout --- e2e-tests/fix_error.spec.ts | 14 +- ...error.spec.ts_fix-error-with-AI-1.aria.yml | 18 +- ...error.spec.ts_fix-error-with-AI-2.aria.yml | 18 +- .../preview_panel/PreviewErrorBanner.test.tsx | 13 +- .../preview_panel/PreviewErrorBanner.tsx | 204 +++++++++--------- 5 files changed, 121 insertions(+), 146 deletions(-) diff --git a/e2e-tests/fix_error.spec.ts b/e2e-tests/fix_error.spec.ts index f5ed1c1756..2b78b75678 100644 --- a/e2e-tests/fix_error.spec.ts +++ b/e2e-tests/fix_error.spec.ts @@ -16,10 +16,10 @@ testSkipIfWindows("fix error with AI", async ({ po }) => { ).toBeHidden(); await po.previewPanel.expandPreviewErrorBanner(); - await expect( - po.page.getByText("Error Line 6 error", { exact: true }), - ).toBeVisible({ timeout: Timeout.MEDIUM }); - await po.page.getByText("Error Line 6 error", { exact: true }).click(); + await expect(po.page.getByText("Line 6 error", { exact: true })).toBeVisible({ + timeout: Timeout.MEDIUM, + }); + await po.page.getByRole("button", { name: "Show details" }).click(); await po.previewPanel.snapshotPreviewErrorBanner({ name: "fix-error-with-AI-2.aria.yml", }); @@ -42,9 +42,9 @@ testSkipIfWindows("copy error message from banner", async ({ po }) => { await po.setUp({ autoApprove: true }); await po.sendPrompt("tc=create-error"); - await expect( - po.page.getByText("Error Line 6 error", { exact: true }), - ).toBeVisible({ timeout: Timeout.MEDIUM }); + await expect(po.page.getByText("Line 6 error", { exact: true })).toBeVisible({ + timeout: Timeout.MEDIUM, + }); await po.previewPanel.clickCopyErrorMessage(); diff --git a/e2e-tests/snapshots/fix_error.spec.ts_fix-error-with-AI-1.aria.yml b/e2e-tests/snapshots/fix_error.spec.ts_fix-error-with-AI-1.aria.yml index 124d6da6b8..7169b0d8b9 100644 --- a/e2e-tests/snapshots/fix_error.spec.ts_fix-error-with-AI-1.aria.yml +++ b/e2e-tests/snapshots/fix_error.spec.ts_fix-error-with-AI-1.aria.yml @@ -1,15 +1,9 @@ -- button "Dismiss error banner": - - img -- button "Collapse error banner" [expanded]: - - img -- 'button "Show full error message: Error Line 6 error"': - - img - - text: "" -- img -- text: "Tip: Check if restarting the app fixes the error." +- paragraph: Line 6 error +- button "Collapse error banner" [expanded] +- button "Dismiss error banner" +- button "Show details" +- text: Try restarting the app. - button "Copy": - img - text: "" -- button "Fix error with AI": - - img - - text: "" \ No newline at end of file +- button "Fix error with AI" \ No newline at end of file diff --git a/e2e-tests/snapshots/fix_error.spec.ts_fix-error-with-AI-2.aria.yml b/e2e-tests/snapshots/fix_error.spec.ts_fix-error-with-AI-2.aria.yml index 75dec54b6e..d22257f22a 100644 --- a/e2e-tests/snapshots/fix_error.spec.ts_fix-error-with-AI-2.aria.yml +++ b/e2e-tests/snapshots/fix_error.spec.ts_fix-error-with-AI-2.aria.yml @@ -1,15 +1,9 @@ -- button "Dismiss error banner": - - img -- button "Collapse error banner" [expanded]: - - img -- 'button /Hide full error message: Error Line 6 error Stack trace: Index \(http:\/\/localhost:\d+\/src\/pages\/Index\.tsx:6:6\)/ [expanded]': - - img - - text: "" -- img -- text: "Tip: Check if restarting the app fixes the error." +- paragraph: Line 6 error +- button "Collapse error banner" [expanded] +- button "Dismiss error banner" +- button "Hide details" [expanded] +- text: "/Error Line 6 error Stack trace: Index \\(http:\\/\\/localhost:\\d+\\/src\\/pages\\/Index\\.tsx:6:6\\) Try restarting the app\\./" - button "Copy": - img - text: "" -- button "Fix error with AI": - - img - - text: "" \ No newline at end of file +- button "Fix error with AI" \ No newline at end of file diff --git a/src/components/preview_panel/PreviewErrorBanner.test.tsx b/src/components/preview_panel/PreviewErrorBanner.test.tsx index 9fb3ef0c00..22d2f0bb76 100644 --- a/src/components/preview_panel/PreviewErrorBanner.test.tsx +++ b/src/components/preview_panel/PreviewErrorBanner.test.tsx @@ -25,7 +25,7 @@ describe("PreviewErrorBanner", () => { />, ); - expect(screen.getByText(/Tip:/)).toBeTruthy(); + expect(screen.getByText("Try restarting the app.")).toBeTruthy(); expect( screen.getByRole("button", { name: "Fix error with AI" }), ).toBeTruthy(); @@ -35,8 +35,8 @@ describe("PreviewErrorBanner", () => { ); expect(screen.getByTestId("preview-error-banner")).toBeTruthy(); - expect(screen.getByText("Error Line 6 error")).toBeTruthy(); - expect(screen.queryByText(/Tip:/)).toBeNull(); + expect(screen.getByText("Line 6 error")).toBeTruthy(); + expect(screen.queryByText("Try restarting the app.")).toBeNull(); expect( screen.queryByRole("button", { name: "Fix error with AI" }), ).toBeNull(); @@ -45,7 +45,7 @@ describe("PreviewErrorBanner", () => { screen.getByRole("button", { name: "Expand error banner" }), ); - expect(screen.getByText(/Tip:/)).toBeTruthy(); + expect(screen.getByText("Try restarting the app.")).toBeTruthy(); expect( screen.getByRole("button", { name: "Fix error with AI" }), ).toBeTruthy(); @@ -78,10 +78,9 @@ describe("PreviewErrorBanner", () => { ); expect(screen.queryByText(/Stack trace/)).toBeNull(); - fireEvent.click( - screen.getByRole("button", { name: /Show full error message/ }), - ); + fireEvent.click(screen.getByRole("button", { name: "Show details" })); expect(screen.getByText(/Stack trace/)).toBeTruthy(); + expect(screen.getByRole("button", { name: "Hide details" })).toBeTruthy(); }); }); diff --git a/src/components/preview_panel/PreviewErrorBanner.tsx b/src/components/preview_panel/PreviewErrorBanner.tsx index 799067befb..d7cc7885db 100644 --- a/src/components/preview_panel/PreviewErrorBanner.tsx +++ b/src/components/preview_panel/PreviewErrorBanner.tsx @@ -1,8 +1,14 @@ import { useState } from "react"; -import { ChevronRight, Lightbulb, Sparkles, X } from "lucide-react"; +import { + CircleAlert, + ChevronDown, + ChevronUp, + Lightbulb, + Sparkles, + X, +} from "lucide-react"; import { CopyErrorMessage } from "@/components/CopyErrorMessage"; import { useStreamChat } from "@/hooks/useStreamChat"; -import { cn } from "@/lib/utils"; interface PreviewErrorBannerProps { error: @@ -21,7 +27,7 @@ export function PreviewErrorBanner({ onAIFix, }: PreviewErrorBannerProps) { const [isBannerCollapsed, setIsBannerCollapsed] = useState(false); - const [isErrorMessageCollapsed, setIsErrorMessageCollapsed] = useState(true); + const [areErrorDetailsVisible, setAreErrorDetailsVisible] = useState(false); const { isStreaming } = useStreamChat(); if (!error) return null; @@ -30,133 +36,115 @@ export function PreviewErrorBanner({ const isInternalDyadError = error.source === "dyad-app"; const isSyncError = error.source === "dyad-sync"; - const getTruncatedError = () => { - const firstLine = error.message.split("\n")[0]; - const snippetLength = 250; - const snippet = error.message.substring(0, snippetLength); - return firstLine.length < snippet.length - ? firstLine - : snippet + (snippet.length === snippetLength ? "..." : ""); - }; + const firstLine = error.message.split("\n")[0]; + const summaryWithoutErrorPrefix = firstLine.replace(/^Error:?\s+/i, ""); + const errorSummary = summaryWithoutErrorPrefix || firstLine; return (
- - - {(isInternalDyadError || isSyncError) && ( -
- {isSyncError ? "Cloud sync issue" : "Internal Dyad error"} +
+
+

+ {errorSummary} +

+ {(isInternalDyadError || isSyncError) && ( + + {isSyncError ? "Cloud sync issue" : "Internal Dyad error"} + + )} +
- )} - {isBannerCollapsed ? ( -
- {getTruncatedError()} -
- ) : ( -
- {/* Error message in the middle */} -
+ + +
+
- - {isErrorMessageCollapsed ? getTruncatedError() : error.message} - - -
+ {!isBannerCollapsed && ( +
+ - {/* Tip message */} -
-
-
- -
- - Tip: - {isDockerError - ? "Make sure Docker Desktop is running and try restarting the app." - : isSyncError - ? "Dyad could not upload your latest local changes to the cloud sandbox. Check your network connection or wait for sync to recover." - : isInternalDyadError - ? "Try restarting the Dyad app or restarting your computer to see if that fixes the error." - : "Check if restarting the app fixes the error."} - -
+ {areErrorDetailsVisible && ( +
+              {error.message}
+            
+ )} + +
+
- {/* Action buttons at the bottom */} {!isDockerError && error.source === "preview-app" && ( -
+
From 9527f46c689f64db49ce0afb1b304e436ea2f271 Mon Sep 17 00:00:00 2001 From: Will Chen <7344640+wwwillchen@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:47:52 -0700 Subject: [PATCH 3/4] chore: apply automated fixes --- src/components/preview_panel/PreviewIframe.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/preview_panel/PreviewIframe.tsx b/src/components/preview_panel/PreviewIframe.tsx index fdea6a190e..999f708e11 100644 --- a/src/components/preview_panel/PreviewIframe.tsx +++ b/src/components/preview_panel/PreviewIframe.tsx @@ -17,7 +17,6 @@ import { ExternalLink, Cloud, Cog, - X, ChevronDown, MousePointerClick, Power, From 4c4403029cd93cde128bb12aa4be85283d0422dc Mon Sep 17 00:00:00 2001 From: Will Chen <7344640+wwwillchen@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:04:40 -0700 Subject: [PATCH 4/4] Address PR review comments - Reset error-banner disclosures when a new error arrives\n- Reuse the shared PreviewError type\n- Add regression coverage for disclosure reset --- .../preview_panel/PreviewErrorBanner.test.tsx | 36 +++++++++++++++++++ .../preview_panel/PreviewErrorBanner.tsx | 10 ++---- .../preview_panel/PreviewIframe.tsx | 5 +-- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/components/preview_panel/PreviewErrorBanner.test.tsx b/src/components/preview_panel/PreviewErrorBanner.test.tsx index 22d2f0bb76..3a9699d05d 100644 --- a/src/components/preview_panel/PreviewErrorBanner.test.tsx +++ b/src/components/preview_panel/PreviewErrorBanner.test.tsx @@ -83,4 +83,40 @@ describe("PreviewErrorBanner", () => { expect(screen.getByText(/Stack trace/)).toBeTruthy(); expect(screen.getByRole("button", { name: "Hide details" })).toBeTruthy(); }); + + it("resets disclosure state when a different error arrives", () => { + const { rerender } = render( + , + ); + + fireEvent.click( + screen.getByRole("button", { name: "Collapse error banner" }), + ); + + const nextError = { + message: "Error A different failure", + source: "preview-app" as const, + }; + rerender( + , + ); + + expect( + screen.getByRole("button", { name: "Collapse error banner" }), + ).toBeTruthy(); + expect(screen.getByText("Try restarting the app.")).toBeTruthy(); + expect( + screen.getByRole("button", { name: "Fix error with AI" }), + ).toBeTruthy(); + }); }); diff --git a/src/components/preview_panel/PreviewErrorBanner.tsx b/src/components/preview_panel/PreviewErrorBanner.tsx index d7cc7885db..b2f8c58d5f 100644 --- a/src/components/preview_panel/PreviewErrorBanner.tsx +++ b/src/components/preview_panel/PreviewErrorBanner.tsx @@ -9,14 +9,10 @@ import { } from "lucide-react"; import { CopyErrorMessage } from "@/components/CopyErrorMessage"; import { useStreamChat } from "@/hooks/useStreamChat"; +import type { PreviewError } from "@/preview_iframe/state"; interface PreviewErrorBannerProps { - error: - | { - message: string; - source: "preview-app" | "dyad-app" | "dyad-sync"; - } - | undefined; + error: PreviewError; onDismiss: () => void; onAIFix: () => void; } @@ -30,8 +26,6 @@ export function PreviewErrorBanner({ const [areErrorDetailsVisible, setAreErrorDetailsVisible] = useState(false); const { isStreaming } = useStreamChat(); - if (!error) return null; - const isDockerError = error.message.includes("Cannot connect to the Docker"); const isInternalDyadError = error.source === "dyad-app"; const isSyncError = error.source === "dyad-sync"; diff --git a/src/components/preview_panel/PreviewIframe.tsx b/src/components/preview_panel/PreviewIframe.tsx index 999f708e11..61dcd1eff7 100644 --- a/src/components/preview_panel/PreviewIframe.tsx +++ b/src/components/preview_panel/PreviewIframe.tsx @@ -1587,14 +1587,15 @@ export const PreviewIframe = ({ />
- {!loading && ( + {!loading && errorMessage && ( sendIframeEvent({ type: "DISMISS" })} onAIFix={() => { if (selectedChatId) { streamMessage({ - prompt: `Fix error: ${errorMessage?.message}`, + prompt: `Fix error: ${errorMessage.message}`, chatId: selectedChatId, }); }