Skip to content

fix(preview): resize hidden browser tabs reliably - #8469

Open
t3dotgg wants to merge 20 commits into
mainfrom
fix/preview-hidden-viewport-v2
Open

fix(preview): resize hidden browser tabs reliably#8469
t3dotgg wants to merge 20 commits into
mainfrom
fix/preview-hidden-viewport-v2

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Aug 28, 2026

Copy link
Copy Markdown
Member

preview_resize could update the server and React layout while a hidden desktop webview kept its old native viewport. Failed or timed-out resizes could also leave the server, client store, and guest on different viewport values.

This fixes #3712. Desktop now applies the logical viewport through CDP while the preview is hidden, scales desktop dimensions for page zoom, and clears native metrics in fill mode. The client uses server revisions and guarded rollback writes so a late response cannot replace a newer viewport. Ordered revisions take priority over wall-clock timestamps, including when the server clock moves backward.

The rebase preserves the current hidden-preview activity lease, recording state, and macOS paintability behavior. Guilherme Barros's original commits from #7303 keep their authorship in this branch.

Tests:

  • vp test run packages/contracts/src/preview.test.ts apps/server/src/preview/Manager.test.ts apps/web/src/browser/browserViewportActions.test.ts apps/web/src/components/preview/previewGuestViewport.test.ts apps/web/src/components/preview/previewViewportReadiness.test.ts apps/web/src/components/preview/previewViewportRollback.test.ts apps/web/src/components/preview/previewAutomationOpenReadiness.test.ts apps/web/src/components/preview/previewAutomationTarget.test.ts apps/web/src/previewStateStore.test.ts apps/desktop/src/preview/Manager.test.ts (206 passed)
  • vp run --filter @t3tools/contracts typecheck
  • vp run --filter t3 typecheck
  • vp run --filter @t3tools/web typecheck
  • vp run --filter @t3tools/desktop typecheck
  • Focused lint, format, and git diff --check

A manual Electron pass is still needed before merge. This audit did not use browser or computer control.

Made with GPT-5.6 Sol using Codex.


Note

Medium Risk
Large cross-layer concurrency and a breaking preview.resize success shape (PreviewResizeResult); behavior is heavily tested but still needs manual Electron validation.

Overview
Fixes preview resizes that updated server/React layout while a hidden desktop webview kept stale native dimensions, and reduces drift after failed or racing resize operations.

Desktop adds human setViewport and agent automationSetViewport IPC paths that apply logical sizes via CDP Emulation.setDeviceMetricsOverride (scaled for tab zoom, cleared in fill mode), persist intent across webview swaps/DevTools, and roll back or reconcile when applies fail or overlap.

Server preview.resize now returns PreviewResizeResult with stateVersion and previousViewport, and rejects stale rollback writes via PreviewResizeConflictError when expectedStateVersion does not match the tab’s viewport revision.

Web tracks per-tab server revisions in previewStateStore, serializes viewport mutations with absolute deadlines so timed-out commits cannot block newer ones, syncs the guest through applyPreviewGuestViewport on webview attach and viewport changes, and on automation resize failure runs server-first guarded rollback before touching the guest CDP override.

Reviewed by Cursor Bugbot for commit 9425d6e. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix preview automation viewport resize with per-tab revisions and deadline-bound mutations

  • Adds per-tab server revision tracking to previewStateStore so stale resize responses and authoritative lists can no longer overwrite newer tab state, even when wall-clock timestamps are out of order
  • Introduces optimistic concurrency control on PreviewManager.resize: requests carry an optional expectedStateVersion (epoch + revision); a mismatch returns PreviewResizeConflictError without mutating state or emitting events, and successful writes return the new version and previous viewport for rollback
  • Replaces relative timeouts with absolute deadlines across the viewport mutation queue (queueBrowserViewportMutation), preview automation polling (waitForRenderedViewport), and visible commits (commitBrowserViewportChange); a front-of-queue mutation that hits its deadline releases later mutations instead of blocking them
  • Adds a guest viewport bridge with new IPC channels (PREVIEW_SET_VIEWPORT_CHANNEL, PREVIEW_AUTOMATION_SET_VIEWPORT_CHANNEL) and applyPreviewGuestViewport to push the authoritative stored viewport to the guest after webview attachment; fill mode maps to a clear request and fixed dimensions map to explicit width/height
  • New automation tabs opened in fill mode now receive DEFAULT_PREVIEW_AUTOMATION_VIEWPORT instead of inheriting a fill setting, and needsPreviewAutomationSessionSync forces a server refresh when the local serverEpoch is null
  • Behavioral Change: PreviewManager.resize now returns PreviewResizeResult instead of PreviewSessionSnapshot; rpc.WsPreviewResizeRpc and ipc.EnvironmentApi.preview.resize are updated to match. Older servers without stateVersion/previousViewport fields remain backward-compatible since both are optional in the decoder. commitBrowserViewportChange now enforces a 15-second deadline from invocation time including queue wait.

Macroscope summarized 9425d6e.

@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2f92fdcd-4721-4ce1-93bf-cc0f3b60e19d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 28, 2026
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 13.4 KiB 13.4 KiB +6 B (+0.0%) 15.1 KiB
Codex Thread snapshot wire 6.9 KiB 6.9 KiB −1 B (−0.0%) 7.3 KiB
Codex Live turn WebSocket wire 6.5 KiB 6.6 KiB +7 B (+0.1%) 7.8 KiB
Codex Live turn WebSocket decoded 57.0 KiB 57.0 KiB 0 B (0.0%) 66.4 KiB
Codex Live turn messages 10 10 0 (0.0%) 21
Claude Total thread wire 13.5 KiB 13.5 KiB +1 B (+0.0%) 15.1 KiB
Claude Thread snapshot wire 6.9 KiB 6.9 KiB −2 B (−0.0%) 7.3 KiB
Claude Live turn WebSocket wire 6.6 KiB 6.6 KiB +3 B (+0.0%) 7.8 KiB
Claude Live turn WebSocket decoded 57.8 KiB 57.8 KiB 0 B (0.0%) 66.4 KiB
Claude Live turn messages 10 10 0 (0.0%) 21

Baseline: fc53b27 · PR result: 9425d6e · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment thread apps/desktop/src/preview/Manager.ts Outdated
Comment thread apps/web/src/components/preview/PreviewAutomationHosts.tsx Outdated
Comment thread apps/desktop/src/preview/Manager.ts
Comment thread apps/desktop/src/preview/Manager.ts
Comment thread apps/web/src/components/preview/previewViewportRollback.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR introduces a substantial native CDP viewport path across desktop, server, and web, including lifecycle replay, queue deadlines, and rollback behavior. Failure cases can leave the server/React viewport out of sync with the hidden guest after CDP rejection or DevTools transitions.

Not approved because:

  • 1 blocking correctness issue found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

@t3dotgg
t3dotgg force-pushed the fix/preview-hidden-viewport-v2 branch from 64bee3d to e8ad8c7 Compare August 28, 2026 01:15
@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 28, 2026
Comment thread apps/desktop/src/preview/Manager.ts
@t3dotgg
t3dotgg force-pushed the fix/preview-hidden-viewport-v2 branch from 8a03158 to b20a0de Compare August 28, 2026 04:59
Comment thread apps/desktop/src/preview/Manager.ts
@t3dotgg
t3dotgg force-pushed the fix/preview-hidden-viewport-v2 branch from 26b965d to ba7f44d Compare August 28, 2026 05:39
Comment thread apps/web/src/browser/browserViewportActions.ts
Comment thread apps/desktop/src/preview/Manager.ts
Comment thread apps/web/src/previewStateStore.ts Outdated
Comment thread apps/desktop/src/preview/Manager.ts Outdated
Comment thread apps/desktop/src/preview/Manager.ts Outdated
@t3dotgg
t3dotgg force-pushed the fix/preview-hidden-viewport-v2 branch from ba7f44d to c59ea4c Compare August 28, 2026 05:50
Comment thread apps/web/src/previewStateStore.ts Outdated
Comment thread apps/web/src/previewStateStore.ts
Comment thread apps/web/src/components/preview/PreviewAutomationHosts.tsx Outdated
Comment thread apps/desktop/src/preview/Manager.ts
Comment thread apps/desktop/src/preview/Manager.ts Outdated
preview_resize only updated the CSS/React chrome. If the browser panel
was hidden, the guest never changed size and wait timed out.

Resize now persists the setting, then applies a CDP device-metrics
override so the guest viewport changes even when the tab is not visible.
A size-only ready check could resolve resize before React committed the
webview geometry. A failed CDP apply also left the persisted snapshot
on the new size.

Resize now waits for the existing ready gates, rolls the snapshot back
when the override fails, and re-applies the committed setting when the
guest attaches.
The restore effect depended on the viewport object, so every preview
event re-sent CDP and flashed the agent-controlling badge. Toolbar
resizes now use a setViewport path that does not take agent control,
and the effect keys only on the viewport size string.
Width-only mobile detection treated 844x390 phones as desktop. The
guest also stayed pinned to the last committed size while the frame
was dragged.

mobile now uses the shortest side. HostedBrowserWebview applies a
frame-throttled CDP override from the effective (including drag)
viewport.
The HostedBrowserWebview override ran one frame after mount, before
registerWebview finished, then swallowed the failure. Crash recovery
and mini-player never retried.

The override now runs after a successful register, and drag updates
wait for hasWebContents.
PreviewView reapplied the committed size after the webview owner already
set the live override. That raced drags and keyboard resizes. The
active-tab effect is gone; HostedBrowserWebview is the single owner.
gbarros-dev and others added 13 commits September 1, 2026 18:11
Device-metrics overrides live on the CDP session, so DevTools and webview
swaps dropped a fixed viewport while the toolbar still showed it. Remember
the last override and re-apply it from restoreControlSession, like color
scheme.

Guest rollback now rechecks runtime/epoch and only persists previous if
the guest actually rolled back, so a replaced tab or a failed CDP clear
cannot desync store and guest.
setDeviceMetricsOverride is a widget DIP size, so page zoom still divides
it. Desktop presets now send setting * zoom so innerWidth matches the
toolbar. Phone sizes stay unscaled because mobile emulation pins zoom to 1.
Apply-failure never put the new override on the guest, so the store still
rolls back even if the guest CDP clear throws. Timeout still rolls the
guest first and only persists previous if that apply succeeds; a failed
store resize puts the guest back on the requested size.
@t3dotgg
t3dotgg force-pushed the fix/preview-hidden-viewport-v2 branch from 1e6deb6 to 9425d6e Compare September 2, 2026 01:17
const setViewport = previewBridge?.setViewport;
if (!setViewport || !hasWebContents) return;
const frame = window.requestAnimationFrame(() => {
void applyPreviewGuestViewport(setViewport, runtimeTabId, guestViewportRef.current).catch(

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 browser/HostedBrowserWebview.tsx:220

When setViewport rejects after a server-side resize, the UI/server remain at the new effectiveViewport while the native guest stays at its previous size. The effect catches the rejection with () => undefined, and because neither guestViewportKey nor hasWebContents changes, there is no retry or rollback; please retry the native update or restore the prior viewport on failure.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/browser/HostedBrowserWebview.tsx around line 220:

When `setViewport` rejects after a server-side resize, the UI/server remain at the new `effectiveViewport` while the native guest stays at its previous size. The effect catches the rejection with `() => undefined`, and because neither `guestViewportKey` nor `hasWebContents` changes, there is no retry or rollback; please retry the native update or restore the prior viewport on failure.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 9425d6e. Configure here.

).pipe(
Effect.tap(() => acceptViewportOverride(prepared.intent)),
Effect.onError(() => rollbackViewportOverride(tabId, prepared.intent, prepared.generation)),
);

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.

Guest rollback fights server viewport

Medium Severity

Human resizes persist the new viewport on the server and in the React layout first, then call setViewport. On CDP failure (DevTools open, debugger attach, transient guest error) desktop marks that intent rejected and restores the previous native metrics, while the renderer swallows the error and does not retry. After DevTools close, restoreControlSession reapplies the rolled-back intent, so the chrome shows the new size and the page stays on the old override until another resize or zoom.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 9425d6e. Configure here.

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

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: preview_resize times out and leaves viewport state internally inconsistent

2 participants