Skip to content

[Bug]: preview_snapshot renders backdrop-filter as mirrored tiles at viewports wider than 640px #20899

Closed
leomontigatti/agent-workflow
#28

Description

@leomontigatti

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. Serve the fixture below locally (python3 -m http.server). It is a page with a fixed overlay using backdrop-filter: blur(4px) and a dialog on top.
  2. Have an agent call preview_open on it with the preview panel hidden, so the tab runs headless, and keep the default 1280×800 viewport.
  3. Call preview_snapshot with save: true and open the PNG.
  4. preview_resize to freeform 640×400 and snapshot again.
Fixture
<!doctype html>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
  body { margin: 0; font: 16px Arial; color: #171717; background: white; }
  aside { position: fixed; inset: 0 auto 0 0; width: 240px; padding: 24px; background: #f4f4f5; }
  main { margin-left: 288px; padding: 24px; }
  .row { display: flex; gap: 12px; margin-bottom: 12px; }
  .cell { flex: 1; padding: 12px; border: 1px solid #d4d4d8; }
  .green { color: #16a34a; } .red { background: #fecaca; }
  .overlay { position: fixed; inset: 0; background: rgb(0 0 0 / 0.1);
             backdrop-filter: blur(4px); display: grid; place-items: center; }
  .dialog { width: 480px; padding: 24px; background: white; border-radius: 12px;
            box-shadow: 0 10px 30px rgb(0 0 0 / 0.2); }
</style>
<aside><p>Sidebar item one</p><p>Sidebar item two</p><p>Sidebar item three</p><p>Sidebar item four</p></aside>
<main>
  <h1>Page behind the dialog</h1>
  <div class="row"><div class="cell">Alpha</div><div class="cell green">Bravo</div><div class="cell red">Charlie</div></div>
  <div class="row"><div class="cell">Delta</div><div class="cell green">Echo</div><div class="cell red">Foxtrot</div></div>
  <div class="row"><div class="cell">Golf</div><div class="cell green">Hotel</div><div class="cell red">India</div></div>
</main>
<div class="overlay"><div class="dialog"><h2>Dialog</h2><p>The page behind should be one blurred copy.</p></div></div>

Expected behavior

The area behind the dialog is one blurred copy of the page, as Playwright's own page.screenshot of the same page at 1280×800 shows it: three columns, the red one on the right.

Actual behavior

At the default 1280×800 viewport, the backdrop holds only the page's top-left quarter (640×400 CSS px), mirrored horizontally and vertically to fill the screen. The heading reads "Page behind the dialaib eht bnided egaP" and the red column is missing. The dialog itself and everything outside the overlay render correctly.

At 640×400 the same page snapshots correctly. A preview_recording of the 1280×800 tab also renders the backdrop correctly.

The snapshot path appears to be the cause. snapshot() computes scale = min(1, MAX_SCREENSHOT_WIDTH / (viewport.width * renderScale)) (ServerBrowserPage.ts:415), so any viewport wider than 640 CSS px gets scale < 1. captureViewport then sends Page.captureScreenshot with a scaled clip (L380-L396), under --disable-gpu (ServerBrowserContexts.ts:90). The backdrop reads back only the scaled-size region, and the blur's mirrored edge mode fills the rest. That is inferred from the output, not traced in Chromium. Every path that captures at scale 1 is correct: a viewport of 640 px or narrower, recordings (scale: 1, no clip), and Playwright's page.screenshot.

One fix that avoids the clip scale: capture at scale 1, then downscale the PNG to MAX_SCREENSHOT_WIDTH afterwards.

This is separate from #16690 (a desktop-drawn tab's PNG is sized by the display DPR) and #9872 (detail follows the panel's on-screen size). Here the tab is headless, the PNG is the expected 1280×800, and the pixels inside a backdrop-filter are wrong. It affects any page with a modal over a blurred backdrop, which is common in shadcn/Radix-style apps. An agent checking such a dialog sees broken rendering that isn't in the app.

Impact

Major degradation or frequent failure

Version or commit

0.1.1-nightly.20261016.3218 (e58a4857130a); the same code is on main at 4d679a63244e.

Environment

Ubuntu 24.04.5, bundled local server, headless tab (panel hidden), chrome-headless-shell 154.0.8037.92. Captures were taken by an agent through the T3 MCP preview tools.

Logs or stack traces

No response

Screenshots, recordings, or supporting files

  1. preview_snapshot, default 1280×800 viewport: backdrop tiled and mirrored.
Image
  1. preview_snapshot, 640×400 viewport: correct.
Image
  1. Playwright page.screenshot, 1280×800, same page: expected.
Image

Workaround

Snapshot at a viewport of 640 CSS px wide or narrower, or take a frame from a preview_recording. Neither gives a snapshot of a desktop-width layout.

Written by Claude Opus 5.5 on behalf of leomontigatti.

Activity

  1. juliusmarminge commented on Oct 17, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Confirmed on main, with one correction: MAX_SCREENSHOT_WIDTH is 1280 output pixels, not 640. Server tabs render at RENDER_SCALE = 2 (apps/server/src/preview/ServerBrowser.ts), so the threshold works out to 640 CSS px.

    • capture() in apps/server/src/preview/ServerBrowserPage.ts computes scale = min(1, 1280 / (viewport.width * 2)). A 1280×800 tab gets 0.5.
    • captureViewport() passes that to Page.captureScreenshot as clip.scale whenever it's below 1. At 640 px or narrower, no clip is sent.
    • The browser runs with --disable-gpu (ServerBrowserContexts.ts), which means software compositing. Software compositing drawing backdrop-filter wrong in a scaled capture fits your result.

    preview_snapshot only takes a tab target, includeImage and save, so there's no full-resolution option to use instead. This isn't a duplicate of #16690, #18593 or #9872, and #19913 only changes desktop-drawn tabs.

    Workarounds until it's fixed:

    • Snapshot at a viewport 640 CSS px wide or narrower. That path never sends clip.scale.
    • Use a preview_recording frame. As you found, it captures correctly.
  2. added
    acceptedfeature request accepted
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 17, 2026
  3. watashiwaaniket commented on Oct 17, 2026

    @watashiwaaniket

    can I get issue assigned to me? I am currently working on the fix. Thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedfeature request acceptedbugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions