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: () => Copy ,
+}));
+
+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 (
+
+
+
+
+
setIsBannerCollapsed((collapsed) => !collapsed)}
+ aria-label={
+ isBannerCollapsed ? "Expand error banner" : "Collapse error banner"
+ }
+ aria-expanded={!isBannerCollapsed}
+ aria-controls="preview-error-banner-content"
+ className="absolute top-1 left-7 p-1 hover:bg-red-100 dark:hover:bg-red-900 rounded"
+ data-testid="preview-error-banner-toggle"
+ >
+
+
+
+ {(isInternalDyadError || isSyncError) && (
+
+ {isSyncError ? "Cloud sync issue" : "Internal Dyad error"}
+
+ )}
+
+ {isBannerCollapsed ? (
+
+ {getTruncatedError()}
+
+ ) : (
+
+ {/* Error message in the middle */}
+
+
+ setIsErrorMessageCollapsed((collapsed) => !collapsed)
+ }
+ aria-label={
+ isErrorMessageCollapsed
+ ? `Show full error message: ${getTruncatedError()}`
+ : `Hide full error message: ${error.message}`
+ }
+ aria-expanded={!isErrorMessageCollapsed}
+ >
+
+
+
+ {isErrorMessageCollapsed ? 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" && (
+
+
+
+
+ Fix error with AI
+
+
+ )}
+
+ )}
+
+ );
+}
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" && (
-
-
-
-
- Fix error with AI
-
-
- )}
-
- );
-};
-
// 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 (
-
-
-
-
setIsBannerCollapsed((collapsed) => !collapsed)}
- aria-label={
- isBannerCollapsed ? "Expand error banner" : "Collapse error banner"
- }
- aria-expanded={!isBannerCollapsed}
- aria-controls="preview-error-banner-content"
- className="absolute top-1 left-7 p-1 hover:bg-red-100 dark:hover:bg-red-900 rounded"
- data-testid="preview-error-banner-toggle"
- >
-
+
-
- {(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 */}
-
+ setIsBannerCollapsed((collapsed) => !collapsed)}
+ aria-label={
+ isBannerCollapsed
+ ? "Expand error banner"
+ : "Collapse error banner"
+ }
+ aria-expanded={!isBannerCollapsed}
+ aria-controls="preview-error-banner-content"
+ className="rounded p-1 text-red-500 transition-colors hover:bg-red-100 hover:text-red-700 dark:text-red-400 dark:hover:bg-red-900 dark:hover:text-red-200"
+ data-testid="preview-error-banner-toggle"
+ >
+ {isBannerCollapsed ? (
+
+ ) : (
+
)}
+
+
-
- setIsErrorMessageCollapsed((collapsed) => !collapsed)
- }
- aria-label={
- isErrorMessageCollapsed
- ? `Show full error message: ${getTruncatedError()}`
- : `Hide full error message: ${error.message}`
- }
- aria-expanded={!isErrorMessageCollapsed}
- >
-
+
+
+
+
-
- {isErrorMessageCollapsed ? getTruncatedError() : error.message}
-
-
-
+ {!isBannerCollapsed && (
+
+
+ setAreErrorDetailsVisible((detailsVisible) => !detailsVisible)
+ }
+ aria-expanded={areErrorDetailsVisible}
+ >
+ {areErrorDetailsVisible ? "Hide details" : "Show details"}
+
- {/* 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}
+
+ )}
+
+
+
+
+ {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 your computer."
+ : "Try restarting the app."}
+
- {/* Action buttons at the bottom */}
{!isDockerError && error.source === "preview-app" && (
-
+
-
+
Fix error with AI
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,
});
}