Repository navigation
[Bug]: preview_resize always times out (and evicts the automation host) when the main window is zoomed, because the guest webview reports innerWidth × hostZoom #12319
Description
Activity
Triage
Confirmed on current
main(4749035bd) and on v0.0.42. This is a real desktop preview-automation bug, not a flake and not already fixed. The report’s diagnosis matches the code, and the measured numbers are exact: zoom level 2.5 is1.2 ** 2.5 = 1.577440977, and1280 × 1.577440977 ≈ 2019,800 × 1.577440977 ≈ 1262.What happens
preview_resizeapplies the server snapshot, then the desktop renderer polls until the guest CSS viewport matches the request within 1 px:const readWebviewViewport = async ( webview: ExecutablePreviewWebview, ): Promise<PreviewRenderedViewportSize | null> => { const value = await webview.executeJavaScript( "({ width: window.innerWidth, height: window.innerHeight })", ); // ... };
// Electron rounds CSS pixels through the guest's fractional zoom/device scale, // so a successfully applied fixed viewport can measure one pixel either way. const tolerance = 1; return ( Math.abs(renderedViewport.width - expectedViewport.width) <= tolerance && Math.abs(renderedViewport.height - expectedViewport.height) <= tolerance );
The
<webview>is styled to the requested size in host CSS pixels (data-preview-css-width/style.widthinHostedBrowserWebview.tsx). Preview-tab zoom is already compensated: layout multiplies the frame bytab.zoomFactor, andassertTabZoomsets the guest to that same factor, so a 1280 CSS request still reportsinnerWidth = 1280when the window zoom is 1.Window zoom is the opposite. Chromium inherits the embedder zoom onto the guest; desktop then resets it to
tab.zoomFactor(defaultbrowserDefaultZoomFactor = 1) so the preview “keeps its own zoom”:zoomMain: Effect.fn("desktop.window.zoomMain")(function* (direction) { // ... webContents.setZoomLevel( direction === "reset" ? 0 : webContents.getZoomLevel() + (direction === "in" ? 0.5 : -0.5), ); // Chromium pushes the new level down to embedded guests, which would zoom // the previewed page along with the app UI. The preview browser keeps its // own zoom, so put each guest back where the preview left it. yield* previewManager.reapplyZoom(); }),
const assertTabZoom = Effect.fn("PreviewManager.assertTabZoom")(function* (tabId: string) { const tab = (yield* SynchronizedRef.get(tabsRef)).get(tabId); if (!tab || tab.webContentsId == null) return; const wc = webContents.fromId(tab.webContentsId); if (!wc || wc.isDestroyed()) return; yield* attempt({ operation: "assertTabZoom", tabId, webContentsId: wc.id }, () => wc.setZoomFactor(tab.zoomFactor), ).pipe(Effect.ignore); });
Same reset on
registerWebview(Manager.ts2190–2196) and on everyreapplyZoom(2617–2619). Result with window zoom 1.577 and guest zoom 1: a frame declared 1280×800 occupies ~2019×1262 device pixels, the guest reports that asinnerWidth/innerHeight, and the ±1 check can never pass. Existing tests lock this in:previewViewportReadiness.test.tsonly allows one pixel of slack;DesktopWindow.test.tsrequiresreapplyZoomafter everyzoomMain;Manager.test.tsexpectssetZoomFactor(tab.zoomFactor)with no window-zoom term.This is not Retina/
devicePixelRatioas a display scale. On a scale-1 display, rendererdevicePixelRatioequals the window zoom factor (the reporter’s1.5774409770965576). On Retina,devicePixelRatioincludes display scale; a default-zoom Retina Mac would already be broken if the guest used full DPR, and it is not. UsemainWindow.webContents.getZoomFactor(), notwindow.devicePixelRatio.Why the host then dies
waitForRenderedViewportburns the full request budget (default 15 s) and never replies. The broker treats an unanswered deadline as host death:const result = yield* Deferred.await(deferred).pipe(Effect.timeoutOption(timeoutMs)); return yield* Option.match(result, { onNone: () => Effect.gen(function* () { // An unanswered request invalidates this connection. Do not replay // actions: the client may have applied them before becoming unreachable. yield* disconnect(connection.clientId, connection.queue); return yield* new PreviewAutomationTimeoutError(requestContext); }),
That is the logged
Preview automation resize timed out after 15000ms.(PreviewAutomationTimeoutErrorinpackages/contracts/src/previewAutomation.ts), thenPreviewAutomationNoAvailableHostErroron every laterpreview_*until the window is reloaded. The host lives only in the Electron renderer (PreviewAutomationHostsreturnsnullwhen!isElectron), so a zoomed desktop window breaks automation even if the agent is driven from the website.Even without automation, the shown “1280 px” page actually lays out at ~2019 CSS px, so device-toolbar breakpoints are wrong whenever the UI is zoomed.
Not a duplicate
Issue Why different #12146 Host gone and never re-registers. This issue is a deterministic cause of the 15 s timeout that trips that eviction. #12273 A best-effort 500 ms metadata statusevicts the host. Same disconnect path, different trigger.#11381 Made unanswered-timeout eviction intentional for liveness. Correct for a dead host; not for a resize that can never succeed. Keep all three open. Fixing the zoom math stops this hang; it does not restore a host after some other timeout, and it does not make the optional metadata read non-evicting.
Suggested fix
Do not only relax
isPreviewViewportReady(divide guestinnerWidthby window zoom). That would make the wait succeed and leave breakpoints wrong.Compensate window zoom the same way preview-tab zoom is already compensated: at assert time, set
guestZoom = tab.zoomFactor * mainWindow.webContents.getZoomFactor()in
assertTabZoom/registerWebview.restoreZoomFactor/reapplyZoom. Keep storedtab.zoomFactoras the preview-only setting (defaultbrowserDefaultZoomFactor). After that, a 1280×800 declaration stays 1280×800 guest CSS at any UI zoom,preview_resize(freeform, preset,fill) becomes ready immediately, and the visible frame still tracks View > Zoom In.Do not use renderer
devicePixelRatioas the guest zoom (wrong on Retina). Snap only if Electron requires it;getZoomFactor()already matches the 0.5-level ladder (1.2 ** level).Tests to add/adjust:
assertTabZoom/reapplyZoomapply the product, nottab.zoomFactoralone, when the main window zoom is ≠ 1.- Preview-tab zoom 1.25 × window zoom 1.577 still yields guest CSS = requested size.
- Keep the existing ±1 readiness tests in CSS pixels; do not widen tolerance to absorb window zoom.
- Existing
DesktopWindow“restores preview zoom after zooming the app” test can stay; it only assertsreapplyZoomis invoked.
Workaround
View > Actual Size (Cmd+0) before preview automation. If the host is already gone, View > Reload (or close/reopen the window). Same as the reporter.
Labels / next
- Type: bug (accepted). Desktop zoom assert + tests; no Electron upgrade.
- Labels:
bug,accepted,via-triage. - Next: land
guestZoom = tab.zoomFactor * mainWindowZoomFactorinassertTabZoom/ register /reapplyZoom. Leave [Bug]: Preview automation host disappears mid-session ("No preview automation host is available for <op> in environment …") and never reconnects #12146 (reconnect) and [Bug]: Optional 500 ms preview metadata timeout disconnects the automation host #12273 (non-evicting metadata) as separate follow-ups.
- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.acceptedfeature request acceptedfeature request acceptedvia-triageFiled through npx t3 triageFiled through npx t3 triage
on Sep 17, 2026 Note
🤖 Claude Opus 5.5 responding on behalf of Victor
Still happens on nightly
0.0.46-nightly.20261005.2702(macOS, main window zoom 1.5). The saved size is 1280×800, the measured size is 1683×1052, andpreview_resizetimes out. #10536 fixes it in my test on currentmain: #10536 (comment)
Before submitting
preview_resizecan never succeed while the main window is zoomed.Area
apps/web (preview automation host in the renderer), with a side effect in apps/server
Steps to reproduce
1.2 ** 2.5 = 1.577;window.devicePixelRatioin the renderer reads1.577on a non-Retina 1920x2160 display).preview_openwith any URL (e.g.https://example.com).preview_resizewith{ mode: "freeform", width: 1440, height: 900 }(any freeform or preset size reproduces).Expected behavior
The resize completes and the guest reports a 1440x900 CSS viewport, independent of the main window zoom.
Actual behavior
preview_resizeblocks for the full timeout and fails withPreview automation resize timed out after 15000ms.BecausePreviewAutomationBroker.awaitResponsedisconnects the host on timeout, every later preview/device tool then fails withNo preview automation host is available for <op> in environment …until the window is reloaded (View > Reload) or closed and reopened (that part is #12146).Root cause, read from the bundled renderer in
app.asar(function names are the minified ones):XU({ setting, appliedSettingKey, declaredViewport, renderedViewport })is true, whererenderedViewportcomes fromwebview.executeJavaScript("({ width: window.innerWidth, height: window.innerHeight })")and must match the requested CSS size within 1 px.PreviewManagerasserts the guest zoom factor totab.zoomFactor(defaultbrowserDefaultZoomFactor = 1) onregisterWebviewand again inreapplyZoomwheneverDesktopWindow.zoomMainchanges the main window zoom.<webview>styledwidth: 1280px(host CSS px) is 2019 device px, and the guest reportsinnerWidth = 2019. Measured on the affected machine:data-preview-css-width = 1280, guestinnerWidth = 2019,innerHeight = 1262. The comparison can never pass, so every resize times out.Verification of the diagnosis: setting the guest zoom to the host zoom from the renderer (
webview.setZoomFactor(window.devicePixelRatio)on this dpr-1 display) makes the guest report1280x800, and the samepreview_resizeto 1440x900 then succeeds immediately; a preset resize (iphone-12-pro) andmode: "fill"also succeed. Resetting the guest zoom to 1 brings the timeout back.Impact
Any user who zooms the T3 Code UI (accessibility, high-resolution portrait monitors, non-Latin scripts) loses all preview automation on the first resize, and the failure mode is a 15 s hang followed by a permanent "no host" state, with no hint that the UI zoom is the cause.
Version or commit
T3 Code (Alpha) 0.0.42 (
CFBundleVersion 0.0.42), app.asar dated 2026-09-16.Environment
macOS (Darwin 25.5.0), Apple Silicon, single 1920x2160 display at scale 1. Agent provider: Codex (when first hit) and Claude Code (reproduction). Local environment, no T3 Connect.
Logs or stack traces
server.trace.ndjson, 2026-09-17 (Asia/Jerusalem), same environment id throughout:No new
PreviewAutomationBroker.connectappears afterwards until the window is reloaded.desktop.trace.ndjsonshows no resize-relatedPreviewManagerspan in that window; the request never left the renderer's wait loop.Screenshots, recordings, or supporting files
Renderer state captured over the local DevTools port while the resize was pending:
{"inner":[532,1349],"dpr":1.5774409770965576,"outer":[840,2129], "webview":{"key":"freeform:1280:800:","cssW":"1280","cssH":"800","zoomFactor":1, "guestInner":{"width":2019,"height":1262}}}Workaround
preview_resizepass and restores correct breakpoints. A proper fix would either compare the rendered viewport in host CSS pixels (divide guestinnerWidthby the embedder zoom factor), or set the guest zoom tobrowserDefaultZoomFactor * mainWindowZoomFactorinassertTabZoom/reapplyZoom.