Skip to content

Commit c649f2d

Browse files
committed
fix(web): pin preview state after settings hydration
- hydrate browser defaults before reading the open request session - prevent reused-tab opens from mixing server epochs 🤖 Co-authored by GPT-5 in Codex via T3 Code
1 parent 8cd9ac3 commit c649f2d

2 files changed

Lines changed: 8 additions & 4 deletions

File tree

BRANCH_DETAILS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Expected behavior:
2727
- A background snapshot that times out before desktop capture begins releases its presentation lease even when Chromium has paused compositor-frame callbacks. Once desktop capture starts, a timed-out snapshot retains its presentation lease until that capture settles, so response timeouts cannot tear down compositor staging beneath an in-flight capture. The desktop snapshot receives the operation's remaining timeout and bounds its control session accordingly.
2828
- The shared preview contract treats snapshot screenshots as nullable. MCP snapshot responses omit image content when capture is unavailable while preserving structured semantic content and explicitly reporting `screenshot: null`; tool descriptions promise a PNG only when capture is available. In `packages/contracts/src/ipc.ts`, the branch's desktop snapshot schema and upstream's create-tab defaults coexist: snapshot calls default an omitted `background` flag to `false`, while tab creation carries viewport, zoom, and color-scheme defaults through the preload and desktop manager.
2929
- The renderer automation consumer reserves response grace before the broker deadline and converts a stalled host operation into a typed `PreviewAutomationTimeoutError` instead of leaving the broker to surface a generic execution failure. Short caller-supplied timeouts retain their full execution budget, and the transition into grace reservation remains monotonic as requested timeouts increase. Requests that ask to open the inline preview use the request's remaining bounded visibility budget rather than a fixed two-second ceiling. Best-effort presentation settling uses a non-throwing remaining-budget read and clamps its 500-millisecond ceiling to that budget. Overlay status calls are themselves bounded by the remaining deadline and revalidate runtime guest identity after awaiting the desktop bridge, while overlay, navigation, and visibility polling clamp each sleep to the remaining deadline. Stable-presentation dwell also contracts to fit short deadlines instead of requiring an impossible fixed 100 milliseconds. Reused empty or failed tabs acknowledge without waiting for a browser surface those states intentionally hide. Visibility timeouts separately report the inline preview's selected tab, the right panel's active surface and open state, the active presentation kind, whether the requested browser surface was registered, and whether it had a presentation rectangle.
30-
- `PreviewAutomationHosts.tsx` resolves `browserDefaults.ts` once at the start of each automation-open request. That single snapshot supplies the new-tab viewport and the automatic floating-preview preference, so creation and presentation cannot observe different settings during one request. Explicit `open` or its deprecated `show` alias remains authoritative; when both are omitted, `autoShowFloatingPreview` decides presentation. The resulting `shouldPresentPreview` value is passed unchanged to `previewAutomationOpenReadiness.ts`, so a reused rendered tab left in the background does not wait for visibility while an explicitly shown tab does. A newly created tab applies its server snapshot and assigned tab id, uses the configured viewport or the branch's deterministic 1280×800 fallback when the snapshot remains `fill`, initiates any requested selection, and acknowledges server-side creation without depending on cold React panel rendering, Electron overlay registration, or page readiness. Its initial URL continues loading exactly once in that same tab; later wait, snapshot, or interaction operations own attachment and page readiness. Reopening an existing shown tab selects both the preview-state tab and its matching inline mini-player surface, then waits for stable presentation and reasserts that selection across same-server route hydration or session reconciliation. A server-epoch change aborts the pending open with `PreviewAutomationTargetUnavailableError`; the old request never adopts the replacement runtime guest.
30+
- `PreviewAutomationHosts.tsx` resolves `browserDefaults.ts` once at the start of each automation-open request, before taking the session snapshot that pins a reused runtime guest. That single settings snapshot supplies the new-tab viewport and the automatic floating-preview preference, so creation and presentation cannot observe different settings during one request, while cold settings hydration cannot mix a pre-await session snapshot with a post-await server epoch. Explicit `open` or its deprecated `show` alias remains authoritative; when both are omitted, `autoShowFloatingPreview` decides presentation. The resulting `shouldPresentPreview` value is passed unchanged to `previewAutomationOpenReadiness.ts`, so a reused rendered tab left in the background does not wait for visibility while an explicitly shown tab does. A newly created tab applies its server snapshot and assigned tab id, uses the configured viewport or the branch's deterministic 1280×800 fallback when the snapshot remains `fill`, initiates any requested selection, and acknowledges server-side creation without depending on cold React panel rendering, Electron overlay registration, or page readiness. Its initial URL continues loading exactly once in that same tab; later wait, snapshot, or interaction operations own attachment and page readiness. Reopening an existing shown tab selects both the preview-state tab and its matching inline mini-player surface, then waits for stable presentation and reasserts that selection across same-server route hydration or session reconciliation. A server-epoch change aborts the pending open with `PreviewAutomationTargetUnavailableError`; the old request never adopts the replacement runtime guest.
3131
- The standard dev runner keeps local navigation and direct backend URLs on `127.0.0.1`. Browser modes use a single origin: they leave client HTTP/WebSocket URLs and generic `HOST` unset so remote sharing and origin-derived HMR keep working, while Vite's default listener and its default backend proxy use explicit IPv4 loopback. Explicit IPv6 backend binds proxy through IPv6 loopback. Desktop mode pins `HOST` and its renderer/backend URLs to `127.0.0.1`; server-only mode keeps direct HTTP/WebSocket URLs on the same IPv4 loopback.
3232
- Retained browser guests subscribe only to render state for their own epoch-scoped runtime tab, plus their thread's active panel and mini-player selection. Presentation or background-capture updates for another tab do not rerender every mounted `HostedBrowserWebview`; selection changes override a stale surface-visible flag so background staging remains nearly transparent and exposes its readiness marker, and staging derives its viewport-fitted rectangle from the target tab's own stable rectangle.
3333
- The primary pairing route watches for later URL-fragment changes while it remains mounted. Navigating an already-loaded `/pair` document to `/pair#token=...` claims each new token once, removes the secret fragment, and runs the normal pairing exchange without requiring a reload or a second desktop window. Multiple tokens received while an exchange is pending are serialized, and the submitting state remains active until every queued exchange settles.

apps/web/src/components/preview/PreviewAutomationHosts.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId })
296296
};
297297
let tabId = request.tabId ?? null;
298298
try {
299+
const openBrowserDefaults =
300+
request.operation === "open" ? await resolveBrowserDefaults() : undefined;
299301
let state = readThreadPreviewState(threadRef);
300302
const needsSessionSync = needsPreviewAutomationSessionSync(state, request.tabId);
301303
if (needsSessionSync) {
@@ -347,10 +349,12 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId })
347349
return await currentStatus(threadRef, tabId);
348350
case "open": {
349351
const input = request.input as PreviewAutomationOpenInput;
350-
const browserDefaults = await resolveBrowserDefaults();
352+
if (!openBrowserDefaults) {
353+
throw new Error("Browser defaults were not resolved for preview open");
354+
}
351355
const shouldPresentPreview = shouldOpenPreviewMiniPlayer(
352356
input,
353-
browserDefaults.autoShowFloatingPreview,
357+
openBrowserDefaults.autoShowFloatingPreview,
354358
);
355359
const resolvedInputUrl = input.url
356360
? resolveBrowserNavigationTarget(environmentId, {
@@ -376,7 +380,7 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId })
376380
...(resolvedInputUrl ? { url: resolvedInputUrl } : {}),
377381
// An agent that didn't state a size gets the user's
378382
// configured default, same as a hand-opened tab.
379-
viewport: browserDefaultOpenViewport(browserDefaults),
383+
viewport: browserDefaultOpenViewport(openBrowserDefaults),
380384
},
381385
});
382386
if (result._tag === "Failure") {

0 commit comments

Comments
 (0)