Desktop preview: preview_click lands off-target after preview_resize (coordinates not adjusted for emulation scale-to-fit)
What happens
After calling preview_resize with an explicit width/height, subsequent preview_click
calls report success but the synthesized pointer events land away from the target element —
scaled away from it, proportionally to the requested viewport size. At the preview panel's
natural size (and after preview_resize with preset: "desktop"), the same clicks land
exactly on target.
I hit this while driving a local web app through the preview tools, spent some time confused
by "Successfully clicked" with nothing happening, and eventually instrumented the page to see
where the events actually go. Writing it up in case it saves someone the same detour — and
apologies in advance if I've misread anything; part of this comes from reading the minified
bundle from the outside.
Environment
- Claude Desktop 1.12603.1.0 (MSIX install), Windows 11 (10.0.26200)
- claude-code runtime 2.1.170
- Preview MCP server bundled with the desktop app (
Claude_Previewtools)
Minimal repro
Serve this page (e.g. python -m http.server 8000) and add it to .claude/launch.json:
<!doctype html>
<meta charset="utf-8">
<div id="t" style="position:fixed;left:150px;top:250px;width:40px;height:40px;background:#c33"></div>
<script>
window.hits = [];
addEventListener("click", e => hits.push({
x: e.clientX, y: e.clientY,
target: e.target.id || e.target.tagName,
trusted: e.isTrusted
}), true);
</script>
preview_start, thenpreview_clickselector#t→window.hitsshows a trusted click at
(170, 270) on #t. Correct.
preview_resizewidth 980, height 920 →preview_click#tagain → the tool reports
success, but window.hits shows the trusted click at roughly (397, 631) on MAIN/HTML
— the target was never hit.
preview_resizepreset: "desktop"("Viewport reset to native size") →preview_click
#t → (170, 270) on #t again.
Measurements
On a preview panel whose natural viewport was 419 CSS px wide, with landing points predicted
beforehand from landing = target_center × (requestedWidth / 419.34):
| State | dpr | Predicted landing | Measured landing | Factor |
| ---------------------------- | --- | ----------------- | ---------------- | ------- |
| natural (no resize) | 1.5 | (170, 270) | (170, 270) | 1.000 |
| resized 1200×800 | 1.5 | (486.5, 772.7) | (486, 773) | ~2.861 |
| resized 980×920 | 1.5 | (397.3, 631.0) | (397, 631) | ~2.336 |
| resized 419×714 (≈ panel) | 2.0 | (170, 270) | (170, 270) | ~1.001 |
| after preset: "desktop" | 1.5 | (170, 270) | (170, 270) | 1.000 |
The factor tracks the requested width: F(1200)/F(980) ≈ 1200/980 within ~0.01%. A second
target position at the 980 state scaled by the same factor through the origin, so it behaves
like a pure scale rather than an offset. The delivered events are real trusted input
(pointerdown/mousedown/pointerup/mouseup/click), which is what makes the miss invisible to the
caller — the tool's success message reflects selector resolution, not delivery.
Two smaller observations from the same session, in case they're intentional:
- Requested widths below 768 appear to switch to mobile emulation implicitly
(devicePixelRatio becomes 2, screen metrics get overridden) even when only a small desktop
viewport was wanted.
- There doesn't seem to be a way to clear emulation via explicit
width/height— only
preset: "desktop" resets ("Viewport reset to native size"); every numeric size re-enables
emulation.
What I think is happening (from reading app.asar — happy to be corrected)
click() measures the element center in page CSS pixels in-page and passes it directly to CDPInput.dispatchMouseEvent:
const r = await this.evaluate(`(function() {
const el = document.querySelector(...);
el.scrollIntoView({ block: 'center', behavior: 'instant' });
const r = el.getBoundingClientRect();
return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
})()`);
return r ? this.clickAt(r.x, r.y, t) : ...;
setViewport() enables Emulation.setDeviceMetricsOverride without a scale argument; when
the emulated viewport is larger than the hosting view, Chromium scales the page to fit, and
the input coordinates (interpreted in view space) end up transformed by the inverse of that
fit factor — which matches the measured requestedWidth / panelWidth exactly. The class keeps
an emulationScale field, but it only mirrors the explicit scale parameter, which the
resize path never passes. If this reading is right, hoverAt, scrollAt and dragFromTo
would be affected the same way, and a possible fix is converting page → view coordinates
(multiply by min(viewW/emulatedW, viewH/emulatedH, 1)) before each Input.dispatch*, or
passing an explicit scale and dividing through it.
Workaround that works today
preview_resize with preset: "desktop" before any click-driven steps restores correct
clicking; resizes can be done afterwards for screenshots. And verifying interactions by an
observable page-state change (rather than the success message) catches this class of issue
immediately.
Happy to re-run any of this or provide the raw logs if useful. Thanks for the preview tools —
outside of this corner they've been genuinely pleasant to work with.