Skip to content

fix(preview): scale desktop CDP viewport overrides by page zoom

MacroscopeApp / Macroscope - UI Consistency failed Aug 23, 2026 in 2m 13s

UI Consistency: 1 issue found

apps/web/src/components/preview/previewGuestViewport.ts — previewGuestViewportOverride decides mobile emulation from the unscaled CSS size, while PreviewManager.deviceMetricsOverride (apps/desktop/src/preview/Manager.ts:2462) re-derives mobile from the zoom-scaled size it receives. For any zoomFactor < 1 (the zoom ladder goes down to 0.25) a desktop viewport such as 1024x768 at zoom 0.5 is sent as 512x384 and the desktop flips it to mobile: true; mobile emulation pins page zoom to 1, so the guest lays out at ~512 CSS px with mobile viewport semantics while the toolbar still reports 1024x768 — reintroducing the toolbar/guest mismatch this PR fixes.

Suggested fix: keep the classification on the CSS-pixel setting on both sides — either transmit the intended mobile flag (or the unscaled size) through DesktopPreviewAutomationSetViewportInputSchema, or move the zoom scaling into PreviewManager, which already knows tab.zoomFactor. An inline comment with details was posted on the PR.

Details

Note

Your check run agent prompt is: .macroscope/check-run-agents/ui-consistency.md
More information about how Check Run Agents work can be found in our Help Center.

Files examined in scope: apps/web/src/components/preview/previewGuestViewport.ts (+ its test), apps/web/src/browser/HostedBrowserWebview.tsx, apps/web/src/components/preview/PreviewAutomationHosts.tsx. Out-of-scope files (apps/desktop/src/preview/Manager.ts, apps/desktop/src/preload.ts, packages/contracts/src/ipc.ts) were read for context only.

Checks performed:

  • No shared-primitive, Tailwind ownership, theme-variant, or global CSS changes are present in this diff; the changed web code is behavioral viewport plumbing, so those rule sections yielded no findings.
  • Verified the previously flagged detach/re-attach gap is now handled: viewportOverridesRef is restored in restoreControlSession and cleared on tab close, with unit coverage for the webview-swap path.
  • Verified the re-apply effect in HostedBrowserWebview is keyed on browserViewportSettingKey(effectiveViewport), hasWebContents, runtimeTabId, and normalizedZoomFactor, and that it tracks the drag-live viewport, so committed and in-drag sizes both reach the guest.
  • Zoom range confirmed via PREVIEW_ZOOM_LEVELS in packages/contracts/src/preview.ts (0.25 … 5.0), which makes the sub-1 zoom case reachable from the preview zoom controls and the default-zoom setting.

Suggested verification gate: a focused unit test in previewGuestViewport.test.ts asserting the emitted override for a desktop-sized setting at a sub-1 zoom stays classified as non-mobile end to end.


🔍 Agent activity — 9 tool calls

💭 Looking at the diff, the in-scope files are the browser preview components and viewport helper. Since these changes are behavioral IPC plumbing rather than styling or shared UI components, this check likely comes back clear. But prior runs on earlier commits of this PR flagged real behavioral issues around viewport overrides and zoom scaling, so I want to verify those were actually resolved at the current head. Checking HostedBrowserWebview.tsx, the effect dependencies look correctly keyed, DevTools detach restoration appears handled through the viewport overrides ref, and webview swap re-application seems covered by the restore control session logic. I'm also verifying the zoom math: if the page zoom factor is 1.25 and we want an effective inner width of 1024, the device metrics override width needs to be 1280 since zoom divides the CSS pixel viewport — and that's what the current code does, so the scaling direction looks correct. But I'm noticing an inconsistency in the mobile detection logic: the web side checks mobile status using the unscaled viewport dimensions to decide whether to apply zoom, while the desktop side re-derives mobile status from the already-scaled dimensions sent through deviceMetricsOverride. In most cases these agree, but I can construct a case where they diverge — an 800x800 setting with a zoom factor of 0.5 would be scaled down to 400x400, which desktop would then classify as mobile, even though the web layer originally treated it as non-mobile since 800x800 is above the threshold. That divergence matters because mobile emulation pins page zoom to 1, so the resulting innerWidth would be 400 rather than the expected 1600, causing a real layout mismatch. I need to check whether zoom factors below 1 are actually reachable in this codebase before treating this as a genuine finding, since it's plausibly in scope under visual and layout preservation.

1. git_tools — zoomFactor

2. browse_code — apps/web/src/browser/HostedBrowserWebview.tsx

💭 Let me check whether the zoom select options in IntegrationsSettings allow values less than 1.

3. browse_code — apps/web/src/components/settings/IntegrationsSettings.tsx

4. git_tools — ZOOM_FACTOR|zoomFactor

5. browse_code — packages/contracts/src/preview.ts

💭 Zoom out down to 0.25 is definitely possible, so I need to check the mismatch between how mobile classification is computed: web uses unscaled CSS dimensions while desktop's deviceMetricsOverride uses the scaled DIP dimensions to decide if min(width,height) is under 768. I'm working through a case where the desktop viewport is 1024x768 at zero zoom to see how this discrepancy plays out. At 0.5x zoom, a 1024x768 desktop viewport gets sent as 512x384, which desktop then flags as mobile since 384 is under the 768 threshold, even though web classified it as not mobile at full scale. This flips the guest into mobile emulation, pinning zoom to 1 and reporting 512x384 CSS pixels instead of the intended 1024x768 — a real regression where zooming out a desktop-sized preview accidentally triggers mobile emulation. I check that mobile settings avoid this since they aren't scaled, but confirm the same flip happens with other desktop sizes like 1440x900 at 0.5x zoom, which also crosses into mobile territory and would trigger mobile-specific behaviors like touch events and viewport meta handling. Since previewGuestViewport.ts falls within the in-scope files, I'm considering fixing this by passing through the correct mobile classification. So the real fix requires transmitting the mobile flag across the IPC boundary, which touches contracts and the desktop Manager — that's outside the scope of a minimal single-file diff. I want to verify how the desktop Manager applies zoom during mobile emulation, checking deviceMetricsOverride and any related zoom logic in Manager.ts, and confirm whether setZoomFactor runs after mobile emulation is enabled. I'm also second-guessing the timing of guestZoomRef's initialization relative to when the registration effect fires, since effects run post-render. I noticed HostedBrowserWebview mutates refs (guestViewportRef, guestZoomRef) directly during render rather than in an effect, which is a React purity violation that could misbehave under StrictMode or concurrent rendering, though it's likely harmless in practice. I also confirmed cleanup on tab close removes the override properly, and the effect's dependency on normalizedZoomFactor while reading guestZoomRef looks fine. Now I'm checking whether the drag path interacts with any of this.

6. git_tools — setZoomFactor|deviceScaleFactor|mobile|zoomFactor

💭 I'm confirming the mismatch: the desktop classifies mobile based on the zoom-scaled transmitted size, while the web decides scaling based on the unscaled CSS size. So at low zoom levels, a desktop-sized viewport gets scaled below the 768px threshold, triggering mobile emulation on the desktop side — but the web's own logic pins zoom to 1 in that mode, meaning window.innerWidth reflects the scaled DIP size instead of the actual toolbar width, causing the guest to render incorrectly. Testing more scenarios: at zoom levels above 1, sizes scale up consistently between web and desktop, so no mismatch occurs there. The bug is specifically triggered by zoom-out scenarios where a legitimately desktop-sized viewport shrinks below the mobile threshold. I'm now considering that the fix might belong in the desktop's handling, since it already has access to the tab's zoom factor and could derive both scaling and mobile classification from that single source of truth. Confirming zoom-out is achievable via PREVIEW_ZOOM_LEVELS starting at 0.25, and if mobile emulation also resets page zoom to 1, the mismatch between the toolbar's reported size and the actual innerWidth still holds regardless of viewport meta behavior — this is a solid finding. I'm checking a few other angles (the hasWebContents gate, potential double-apply flicker) but they don't turn up additional issues. I'm also considering React 18's concurrent rendering possibly causing stale refs in HostedBrowserWebview, but that seems minor enough to skip. I want to trace the rollback path in PreviewAutomationHosts, and verify whether the guestViewportKey used in HostedBrowserWebview's effect dependencies actually incorporates width and height so resize changes get properly detected.

7. browse_code — apps/web/src/browser/browserViewportLayout.ts

💭 I need to pinpoint the exact lines in previewGuestViewport.ts for the previewGuestViewportOverride function so I can attach the inline comment correctly, so let me check the actual file content first.

8. browse_code — apps/web/src/components/preview/previewGuestViewport.ts

💭 I'll post an inline comment on line 32 since it's a new file and the RIGHT side lines exist in the diff, keeping the comment concise.

9. modify_pr — One finding: the mobile/desktop classification split between this helper and PreviewManager.deviceMetricsOverride diverges once the tab is zoomed out, flipping desktop viewports into mobile emulation.


Agent Credits: 82 credits