Skip to content

Fix preview navigation black screen when selecting routes from dropdown#2610

Merged
wwwillchen merged 3 commits into
dyad-sh:mainfrom
wwwillchen-bot:agent-2428-1770745558
Feb 12, 2026
Merged

Fix preview navigation black screen when selecting routes from dropdown#2610
wwwillchen merged 3 commits into
dyad-sh:mainfrom
wwwillchen-bot:agent-2428-1770745558

Conversation

@wwwillchen-bot

@wwwillchen-bot wwwillchen-bot commented Feb 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Changed navigateToRoute() to use postMessage + location.replace() pattern instead of direct location.href assignment
  • This matches the approach used by back/forward navigation buttons and provides smooth navigation without the black screen flicker and sidebar state reset
  • Also added proper currentIframeUrlRef and preservedUrls updates for HMR remount consistency

Fixes #2428

Test plan

  • Build succeeds
  • Unit tests pass (784 tests)
  • E2E tests pass for preview navigation (npm run e2e -- --grep "preview navigation")
  • Manual testing: Open a multi-page app, use the route dropdown to navigate between routes - should no longer see black screen flicker or sidebar collapse

🤖 Generated with Claude Code


Open with Devin

Summary by cubic

Fixes #2428: removes black screen flicker and sidebar collapse when selecting routes from the preview dropdown. Navigation now uses postMessage + location.replace and keeps the iframe src stable across SPA navigation and HMR.

  • Bug Fixes
    • Use postMessage + location.replace to navigate; prevents flicker and preserves sidebar state.
    • Freeze iframe src with useMemo and same-origin check so SPA nav/HMR don’t reset it; added e2e test to verify src remains unchanged.
    • Sync navigation state: history, canGoBack/canGoForward, currentIframeUrlRef, and per-app preservedUrls (clears on root).

Written for commit 1bdd763. Summary will update on new commits.

…wn (dyad-sh#2428)

Changed navigateToRoute() to use postMessage + location.replace() pattern
instead of direct location.href assignment. This matches the approach used
by back/forward navigation buttons and provides smooth navigation without
the black screen flicker and sidebar state reset reported by users.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @wwwillchen-bot, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses and resolves a significant user experience issue where navigating between routes in the preview panel via the dropdown would cause a disruptive black screen flicker and reset the sidebar state. By refactoring the navigation mechanism to use a more robust postMessage approach, the changes ensure a seamless and consistent user experience, particularly important for multi-page applications and during development with HMR.

Highlights

  • Improved Preview Navigation: Changed the navigateToRoute() function to utilize a postMessage and location.replace() pattern for iframe navigation, aligning with the approach used by back/forward buttons.
  • Eliminated Black Screen Flicker: The new navigation method provides smoother transitions, effectively resolving the black screen flicker and sidebar state reset previously experienced when selecting routes from the dropdown.
  • Enhanced HMR Consistency: Added updates for currentIframeUrlRef and preservedUrls to ensure consistent state management across Hot Module Replacement (HMR) remounts.
Changelog
  • src/components/preview_panel/PreviewIframe.tsx
    • Replaced direct location.href assignment with postMessage for iframe navigation to enable location.replace() behavior.
    • Introduced logic to update currentIframeUrlRef with the new URL after navigation.
    • Implemented updates to preservedUrls to maintain application state across HMR remounts, clearing the URL for root navigation or updating for specific routes.
Activity
  • Build succeeded.
  • All 784 unit tests passed.
  • E2E tests specifically for preview navigation passed.
  • Manual testing confirmed that navigating between routes in a multi-page app no longer shows black screen flicker or sidebar collapse.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@wwwillchen

Copy link
Copy Markdown
Collaborator

@BugBot run

@greptile-apps

greptile-apps Bot commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Overview

Greptile Summary

Changed route dropdown navigation from direct location.href assignment to postMessage + location.replace() pattern to prevent black screen flicker and sidebar state reset when navigating between routes.

Key changes:

  • navigateToRoute() now uses postMessage with type: "navigate" instead of directly setting location.href
  • Added useMemo to freeze iframe src attribute between React re-renders, preventing duplicate navigations when SPA navigation triggers state updates
  • Updated currentIframeUrlRef and preservedUrls state in navigateToRoute() to maintain consistency across HMR remounts
  • Added E2E test verifying iframe src remains stable during SPA navigation

The postMessage target origin issue flagged in the previous thread (using "*") remains unaddressed - while dyad-shim.js validates incoming URLs, the sender should still use a specific origin for defense-in-depth.

Confidence Score: 4/5

  • Safe to merge with minor security consideration already flagged in previous thread
  • Implementation correctly addresses the black screen issue by using the postMessage pattern consistently with back/forward navigation. The useMemo optimization prevents duplicate navigations. E2E test coverage validates the fix. The postMessage security issue using wildcard origin was already raised in previous review thread and is mitigated by receiver-side validation in dyad-shim.js.
  • No files require special attention

Important Files Changed

Filename Overview
src/components/preview_panel/PreviewIframe.tsx Changed navigateToRoute() to use postMessage + location.replace() instead of direct location.href, added useMemo to freeze iframe src between remounts, and updated navigation state management
e2e-tests/refresh.spec.ts Added E2E test to verify SPA navigation inside iframe doesn't change the iframe src attribute, ensuring the fix works correctly

Sequence Diagram

sequenceDiagram
    participant User
    participant PreviewIframe as PreviewIframe.tsx
    participant Iframe as Preview Iframe
    participant Shim as dyad-shim.js

    Note over User,Shim: Route Dropdown Navigation Flow

    User->>PreviewIframe: Select route from dropdown
    PreviewIframe->>PreviewIframe: navigateToRoute(path)
    PreviewIframe->>PreviewIframe: Build full URL from appUrl + path
    PreviewIframe->>Iframe: postMessage({type: "navigate", url})
    Iframe->>Shim: Receive navigate message
    Shim->>Shim: Validate URL (http/https only)
    Shim->>Iframe: location.replace(url)
    Iframe-->>Shim: Navigation complete
    Shim->>PreviewIframe: postMessage({type: "pushState/replaceState"})
    PreviewIframe->>PreviewIframe: Update navigation history
    PreviewIframe->>PreviewIframe: Update currentIframeUrlRef
    PreviewIframe->>PreviewIframe: Update preservedUrls for HMR
    
    Note over PreviewIframe: useMemo keeps iframe src stable<br/>during React re-renders
Loading

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment thread src/components/preview_panel/PreviewIframe.tsx

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request addresses a UI flicker during preview navigation by replacing "location.href" with a "postMessage"-based approach, which is a great improvement for the user experience and correctly handles preserving the URL state for HMR consistency. However, this implementation introduces a medium-severity security vulnerability by not specifying a target origin for "postMessage". This should be remediated by replacing the wildcard "*" with the specific origin of the application URL to prevent potential information disclosure.

Comment thread src/components/preview_panel/PreviewIframe.tsx
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Dyadbot Code Review Summary

Found 2 new issue(s) flagged by 3 independent reviewers (consensus voting: 2+ agents must agree, at least one rating MEDIUM+).

Summary

Severity Count
🔴 HIGH 0
🟡 MEDIUM 2
🟢 LOW 0

Issues to Address

Severity File Issue
🟡 MEDIUM src/components/preview_panel/PreviewIframe.tsx:963 Missing origin validation on preservedUrls update (inconsistency with back/forward handlers)
🟡 MEDIUM src/components/preview_panel/PreviewIframe.tsx:937 URL construction may produce malformed URL if path lacks leading /
🟢 Low Priority Issues (3 items, below consensus threshold)
  • Root path check inconsistency - src/components/preview_panel/PreviewIframe.tsx:965 — checks raw path string (=== "/") instead of parsed .pathname, inconsistent with back/forward handlers
  • Missing try/catch on new URL(appUrl) - src/components/preview_panel/PreviewIframe.tsx:936 — other handlers protect equivalent calls
  • Inconsistent postMessage payload - src/components/preview_panel/PreviewIframe.tsx:941 — back/forward include direction field, this handler omits it

See inline comments for details.

Generated by Dyadbot code review (3 independent agents with consensus voting)

Comment thread src/components/preview_panel/PreviewIframe.tsx
Comment thread src/components/preview_panel/PreviewIframe.tsx

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="src/components/preview_panel/PreviewIframe.tsx">

<violation number="1" location="src/components/preview_panel/PreviewIframe.tsx:946">
P2: Using `"*"` as the `postMessage` target origin is a security risk - it allows any page currently loaded in the iframe to receive the navigation command and URL. Since `baseUrl` is already calculated from `appUrl.origin` on line 936, use it as the target origin to restrict message delivery to the intended recipient only.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/components/preview_panel/PreviewIframe.tsx
@github-actions

Copy link
Copy Markdown
Contributor

✅ Claude Code completed successfully

Summary

  • Fixed 5 review comments (all resolved)
  • CI: E2E failures are pre-existing flaky tests (not related to this PR)

Review Comments Addressed

  1. postMessage("*") security issue (flagged by greptile-apps, gemini-code-assist, cubic-dev-ai) — Replaced wildcard "*" target origin with specific origin (baseUrl / appUrl.origin) in navigateToRoute, handleNavigateBack, and handleNavigateForward
  2. Missing origin validation in preservedUrls update (flagged by github-actions) — Added try/catch with origin validation consistent with back/forward handlers, using parsed .pathname instead of raw path string
  3. URL path normalization (flagged by github-actions) — Added normalizedPath to ensure paths without leading / are handled correctly (e.g. React Router <Route path="about">)

CI Status

  • Build: ✅ Pass
  • Lint/Type checks: ✅ Pass
  • E2E tests: ⚠️ 15 failures (10 flaky/retried successfully) — all failures are in unrelated test files and also fail on main branch. The refresh.spec.ts test (most relevant to this PR) passed on retry.
Details

Review threads resolved:

  • PRRT_kwDOOXuAps5tziDj (greptile-apps) — postMessage security
  • PRRT_kwDOOXuAps5tzjBw (gemini-code-assist) — postMessage security
  • PRRT_kwDOOXuAps5tzjSk (github-actions) — origin validation consistency
  • PRRT_kwDOOXuAps5tzjWz (github-actions) — path normalization
  • PRRT_kwDOOXuAps5tzmKb (cubic-dev-ai) — postMessage security

Changes made in PreviewIframe.tsx:

  • navigateToRoute: Added path normalization, replaced "*" with baseUrl, added origin validation with try/catch for preservedUrls
  • handleNavigateBack: Replaced "*" with new URL(appUrl || targetUrl).origin
  • handleNavigateForward: Replaced "*" with new URL(appUrl || targetUrl).origin

Workflow run

@github-actions github-actions Bot added cc:done needs-human:review-issue ai agent flagged an issue that requires human review and removed cc:pending labels Feb 10, 2026
@wwwillchen

Copy link
Copy Markdown
Collaborator

@BugBot run

@wwwillchen

Copy link
Copy Markdown
Collaborator

@BugBot run

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 7 additional findings in Devin Review.

Open in Devin Review

Comment on lines +983 to +1000
const iframeSrc = useMemo(() => {
if (!appUrl) {
return undefined;
}

const currentUrl = currentIframeUrlRef.current;
if (!currentUrl) {
return appUrl;
}

try {
const currentOrigin = new URL(currentUrl).origin;
const appOrigin = new URL(appUrl).origin;
return currentOrigin === appOrigin ? currentUrl : appUrl;
} catch {
return appUrl;
}
}, [appUrl, reloadKey, selectedAppId]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 useMemo reads stale ref on app switch when origins match, producing wrong iframe src

The iframeSrc useMemo reads currentIframeUrlRef.current during render, but the ref is only reset to the new app's base URL in a useEffect (which runs after render). When selectedAppId changes, the useMemo recomputes with the old ref value. If the old and new apps share the same origin (e.g., both on localhost:3000), the origin check passes and the memo returns the old app's route URL instead of the new app's base URL. Subsequent renders don't fix this because the useMemo deps (appUrl, reloadKey, selectedAppId) don't change again.

Root Cause and Impact

Sequence when switching apps with the same origin:

  1. selectedAppId and appUrl change → useMemo recomputes.
  2. currentIframeUrlRef.current still holds the previous app's URL (e.g. http://localhost:3000/about).
  3. Origin check at line 994-996 sees matching origins → returns the stale URL.
  4. The useEffect at PreviewIframe.tsx:725-735 runs after render and sets currentIframeUrlRef.current = appUrl, but this doesn't trigger a useMemo recomputation because none of its deps changed.
  5. The iframe src is set to the old app's route URL for the lifetime of the component (until a reload or another appUrl change).

Impact: When two apps happen to share the same origin (e.g., one app stops and another starts on the same port), the iframe would load the wrong route from the previous app. In practice this is an uncommon edge case because Dyad typically assigns different ports to different apps.

Prompt for agents
The useMemo for iframeSrc reads currentIframeUrlRef.current, but this ref is only updated in a useEffect that runs after render. When selectedAppId changes and the old/new apps share the same origin, the memo returns a stale URL. To fix this, either: (1) Reset currentIframeUrlRef.current synchronously during render (before the useMemo) when selectedAppId or appUrl changes, using a pattern like checking prevAppUrlRef inline. For example, add a synchronous check before the useMemo: if (appUrl && appUrl !== prevAppUrlRef.current) { currentIframeUrlRef.current = appUrl; prevAppUrlRef.current = appUrl; } Or (2) Add a separate state variable (e.g. iframeSrcVersion) that gets incremented in the useEffect alongside the ref reset, and include it in the useMemo dependency array to force recomputation.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Dyadbot Code Review Summary

Found 3 consensus issue(s) flagged by 3 independent reviewers.
(1 issue skipped — already commented)

Summary

Severity Count
🔴 HIGH 0
🟡 MEDIUM 2
🟢 LOW 0

Issues to Address

Severity File Issue
🟡 MEDIUM src/components/preview_panel/PreviewIframe.tsx:936 Unguarded new URL(appUrl) can throw — inconsistent with other handlers
🟡 MEDIUM src/components/preview_panel/PreviewIframe.tsx:983 useMemo reads a mutable ref that deps cannot track — fragile implicit contract

Already Commented (1 issue)

  • Missing origin validation/try-catch on preservedUrls updatesrc/components/preview_panel/PreviewIframe.tsx:963 (already flagged by a previous review)
🟢 Low Priority Issues (4 items, no consensus)
  • No validation that path starts with / before URL concatenationsrc/components/preview_panel/PreviewIframe.tsx:937 (1/3 reviewers, already commented by previous review)
  • Comment misattributes the flicker fix to location.replace() alonesrc/components/preview_panel/PreviewIframe.tsx:939 (1/3 reviewers)
  • Preserved URL update logic is copy-pasted 5 timessrc/components/preview_panel/PreviewIframe.tsx (1/3 reviewers)
  • Test setup pattern duplicated across three testse2e-tests/refresh.spec.ts:157 (1/3 reviewers)

See inline comments for details on MEDIUM issues.

Generated by Dyadbot multi-agent code review (3 independent reviewers with consensus voting)

Comment on lines 936 to 937
const baseUrl = new URL(appUrl).origin;
const newUrl = `${baseUrl}${path}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM — Unguarded new URL(appUrl) can throw

3/3 reviewers flagged this (2 MEDIUM, 1 LOW)

new URL(appUrl).origin on line 936 will throw a TypeError if appUrl is malformed. Every other navigation handler in this component (handleNavigateBack, handleNavigateForward, handleReload, pushState/replaceState handlers) wraps URL parsing in a try/catch. This is the only handler that doesn't, making it an inconsistency and a potential crash site during edge cases like app startup transitions.

Suggested change
const baseUrl = new URL(appUrl).origin;
const newUrl = `${baseUrl}${path}`;
try {
const baseUrl = new URL(appUrl).origin;
const newUrl = `${baseUrl}${path}`;

The closing } catch { console.error('Invalid appUrl during route navigation'); return; } should wrap the rest of the function body before the final } of the if block.

Comment on lines +983 to +1000
const iframeSrc = useMemo(() => {
if (!appUrl) {
return undefined;
}

const currentUrl = currentIframeUrlRef.current;
if (!currentUrl) {
return appUrl;
}

try {
const currentOrigin = new URL(currentUrl).origin;
const appOrigin = new URL(appUrl).origin;
return currentOrigin === appOrigin ? currentUrl : appUrl;
} catch {
return appUrl;
}
}, [appUrl, reloadKey, selectedAppId]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUMuseMemo reads a mutable ref but deps cannot track ref mutations

3/3 reviewers flagged this (3 MEDIUM)

This useMemo reads currentIframeUrlRef.current (line 988), but refs are not reactive — changes to the ref do NOT trigger re-computation of the memo. The dependency array [appUrl, reloadKey, selectedAppId] relies on the assumption that one of these values always changes whenever the ref needs to be re-read.

After navigateToRoute or back/forward handlers update the ref, none of these deps change, so the memo returns a stale value. This appears to be intentional (the comment says "freeze iframe src"), and it's the actual mechanism that prevents the black screen flicker. However, the coupling is fragile and undocumented — a future maintainer could easily break this by adding a code path that updates the ref expecting the memo to recompute, or by using iframeSrc where they actually need the current URL.

Suggestion: Add a comment directly on the dependency array documenting this intentional design:

Suggested change
const iframeSrc = useMemo(() => {
if (!appUrl) {
return undefined;
}
const currentUrl = currentIframeUrlRef.current;
if (!currentUrl) {
return appUrl;
}
try {
const currentOrigin = new URL(currentUrl).origin;
const appOrigin = new URL(appUrl).origin;
return currentOrigin === appOrigin ? currentUrl : appUrl;
} catch {
return appUrl;
}
}, [appUrl, reloadKey, selectedAppId]);
// Freeze iframe src between remounts so in-iframe SPA navigation (pushState/replaceState)
// doesn't cause React to set a new src and trigger a second full navigation flicker.
const iframeSrc = useMemo(() => {
if (!appUrl) {
return undefined;
}
const currentUrl = currentIframeUrlRef.current;
if (!currentUrl) {
return appUrl;
}
try {
const currentOrigin = new URL(currentUrl).origin;
const appOrigin = new URL(appUrl).origin;
return currentOrigin === appOrigin ? currentUrl : appUrl;
} catch {
return appUrl;
}
// NOTE: currentIframeUrlRef.current is intentionally read as a side-channel
// and is NOT in the dependency array. This memo only re-evaluates when appUrl,
// reloadKey, or selectedAppId change. Navigation handlers (navigateToRoute,
// handleNavigateBack, etc.) update the ref synchronously but do NOT trigger
// memo recomputation — this is what prevents the iframe src from being reset
// during SPA navigation, which would cause a visible black screen flicker.
// Do NOT use iframeSrc to read the current iframe URL; use currentIframeUrlRef.current instead.
}, [appUrl, reloadKey, selectedAppId]);

@github-actions

Copy link
Copy Markdown
Contributor

🎭 Playwright Test Results

⚠️ WARNING: Missing Test Shards!

Some test shards did not report results. This may indicate CI failures or timeouts.

  • 🍎 macOS: found 2/4 shards (2 missing)
  • 🪟 Windows: found 0/4 shards (4 missing)

❌ Some tests failed

OS Passed Failed Flaky Skipped
🍎 macOS 224 3 9 6

Summary: 224 passed, 3 failed, 9 flaky, 6 skipped

Failed Tests

🍎 macOS

  • security_review.spec.ts > security review - edit and use knowledge
    • Error: expect(string).toMatchSnapshot(expected) failed
  • select_component.spec.ts > select component next.js
    • Error: expect(locator).toBeVisible() failed
  • template-create-nextjs.spec.ts > create next.js app
    • Error: expect(locator).toBeVisible() failed

📋 Re-run Failing Tests (macOS)

Copy and paste to re-run all failing spec files locally:

npm run e2e \
  e2e-tests/security_review.spec.ts \
  e2e-tests/select_component.spec.ts \
  e2e-tests/template-create-nextjs.spec.ts

⚠️ Flaky Tests

🍎 macOS

  • chat_history.spec.ts > should open, navigate, and select from history menu (passed after 1 retry)
  • debugging_logs.spec.ts > console logs should appear in the console (passed after 1 retry)
  • local_agent_ask.spec.ts > local-agent ask mode (passed after 1 retry)
  • local_agent_basic.spec.ts > local-agent - dump request (passed after 1 retry)
  • new_chat.spec.ts > new chat (second button) (passed after 1 retry)
  • refresh.spec.ts > preview navigation - forward and back buttons work (passed after 1 retry)
  • setup_flow.spec.ts > Setup Flow > setup banner shows correct state when node.js is installed (passed after 1 retry)
  • setup.spec.ts > setup ai provider (passed after 1 retry)
  • thinking_budget.spec.ts > thinking budget (passed after 1 retry)

📊 View full report

@wwwillchen wwwillchen merged commit fb73c20 into dyad-sh:main Feb 12, 2026
8 of 9 checks passed
azizmejri1 pushed a commit to azizmejri1/dyad that referenced this pull request Feb 12, 2026
…wn (dyad-sh#2610)

## Summary
- Changed `navigateToRoute()` to use `postMessage` +
`location.replace()` pattern instead of direct `location.href`
assignment
- This matches the approach used by back/forward navigation buttons and
provides smooth navigation without the black screen flicker and sidebar
state reset
- Also added proper `currentIframeUrlRef` and `preservedUrls` updates
for HMR remount consistency

Fixes dyad-sh#2428

## Test plan
- [x] Build succeeds
- [x] Unit tests pass (784 tests)
- [x] E2E tests pass for preview navigation (`npm run e2e -- --grep
"preview navigation"`)
- Manual testing: Open a multi-page app, use the route dropdown to
navigate between routes - should no longer see black screen flicker or
sidebar collapse

🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- devin-review-badge-begin -->

---

<a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2610"
target="_blank">
  <picture>
<source media="(prefers-color-scheme: dark)"
srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1">
<img
src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1"
alt="Open with Devin">
  </picture>
</a>
<!-- devin-review-badge-end -->

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes dyad-sh#2428: removes black screen flicker and sidebar collapse when
selecting routes from the preview dropdown. Navigation now uses
postMessage + location.replace and keeps the iframe src stable across
SPA navigation and HMR.

- **Bug Fixes**
- Use postMessage + location.replace to navigate; prevents flicker and
preserves sidebar state.
- Freeze iframe src with useMemo and same-origin check so SPA nav/HMR
don’t reset it; added e2e test to verify src remains unchanged.
- Sync navigation state: history, canGoBack/canGoForward,
currentIframeUrlRef, and per-app preservedUrls (clears on root).

<sup>Written for commit 1bdd763.
Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Will Chen <willchen90@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cc:done needs-human:review-issue ai agent flagged an issue that requires human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[session report] <Preview mode failure>

2 participants