| author | |
| committer | |
| log | ff1761012af46ceb5f1ecc9b2be00aae288a1691 |
| tree | a1a0c899a9e22f4a9a81a3776f3d401e705bdbc9 |
| parent | a40aef4ccbc10b7bcd7bc817151db8d5a9b1c257 |
| signature | Signed by PGP key B5690EEEBB952194 |
9 files changed, 113 insertions(+), 31 deletions(-)
apps/web/src/browser/ElectronBrowserHost.tsx+4-1| ... | ... | @@ -29,6 +29,8 @@ export function ElectronBrowserHost() { |
| 29 | 29 | previewState.serverEpoch, |
| 30 | 30 | snapshot.tabId, |
| 31 | 31 | ), |
| 32 | pictureInPicture: | |
| 33 | previewState.desktopByTabId[snapshot.tabId]?.pictureInPicture ?? false, | |
| 32 | 34 | zoomFactor: previewState.desktopByTabId[snapshot.tabId]?.zoomFactor ?? 1, |
| 33 | 35 | })) |
| 34 | 36 | : []; |
| ... | ... | @@ -80,7 +82,7 @@ export function ElectronBrowserHost() { |
| 80 | 82 | if (!isElectron) return null; |
| 81 | 83 | return ( |
| 82 | 84 | <div className="contents" data-electron-browser-host> |
| 83 | {sessions.map(({ threadRef, snapshot, runtimeTabId, zoomFactor }) => { | |
| 85 | {sessions.map(({ threadRef, snapshot, runtimeTabId, pictureInPicture, zoomFactor }) => { | |
| 84 | 86 | const url = snapshot.navStatus._tag === "Idle" ? null : snapshot.navStatus.url; |
| 85 | 87 | return ( |
| 86 | 88 | <HostedBrowserWebview |
| ... | ... | @@ -90,6 +92,7 @@ export function ElectronBrowserHost() { |
| 90 | 92 | runtimeTabId={runtimeTabId} |
| 91 | 93 | initialUrl={url} |
| 92 | 94 | viewport={snapshot.viewport ?? FILL_PREVIEW_VIEWPORT} |
| 95 | pictureInPicture={pictureInPicture} | |
| 93 | 96 | zoomFactor={zoomFactor} |
| 94 | 97 | /> |
| 95 | 98 | ); |
apps/web/src/browser/HostedBrowserWebview.tsx+11-1| ... | ... | @@ -9,6 +9,7 @@ import { usePreviewBridge } from "~/components/preview/usePreviewBridge"; |
| 9 | 9 | import { cn } from "~/lib/utils"; |
| 10 | 10 | |
| 11 | 11 | import { resolveBrowserSurfacePanelRect, useBrowserSurfaceStore } from "./browserSurfaceStore"; |
| 12 | import { useActiveBrowserRecordingTabIds } from "./browserRecording"; | |
| 12 | 13 | import { |
| 13 | 14 | browserViewportSettingKey, |
| 14 | 15 | resolveBrowserViewportLayout, |
| ... | ... | @@ -47,9 +48,11 @@ export function HostedBrowserWebview(props: { |
| 47 | 48 | readonly runtimeTabId: string; |
| 48 | 49 | readonly initialUrl: string | null; |
| 49 | 50 | readonly viewport: PreviewViewportSetting; |
| 51 | readonly pictureInPicture: boolean; | |
| 50 | 52 | readonly zoomFactor: number; |
| 51 | 53 | }) { |
| 52 | const { threadRef, tabId, runtimeTabId, initialUrl, viewport, zoomFactor } = props; | |
| 54 | const { threadRef, tabId, runtimeTabId, initialUrl, viewport, pictureInPicture, zoomFactor } = | |
| 55 | props; | |
| 53 | 56 | const config = usePreviewWebviewConfig(threadRef.environmentId); |
| 54 | 57 | const [initialSrc] = useState(() => initialUrl ?? "about:blank"); |
| 55 | 58 | const tabLeaseRef = useRef<AcquiredDesktopTab | null>(null); |
| ... | ... | @@ -70,6 +73,10 @@ export function HostedBrowserWebview(props: { |
| 70 | 73 | }; |
| 71 | 74 | }), |
| 72 | 75 | ); |
| 76 | const backgroundActivity = useBrowserSurfaceStore( | |
| 77 | (state) => (state.activityByTabId[runtimeTabId] ?? 0) > 0, | |
| 78 | ); | |
| 79 | const recordingActive = useActiveBrowserRecordingTabIds().has(runtimeTabId); | |
| 73 | 80 | usePreviewBridge({ threadRef, tabId, runtimeTabId }); |
| 74 | 81 | |
| 75 | 82 | useEffect(() => { |
| ... | ... | @@ -231,8 +238,10 @@ export function HostedBrowserWebview(props: { |
| 231 | 238 | |
| 232 | 239 | if (!config) return null; |
| 233 | 240 | |
| 241 | const renderingActive = active || backgroundActivity || pictureInPicture || recordingActive; | |
| 234 | 242 | const wrapperStyle = resolveHostedBrowserWebviewWrapperStyle({ |
| 235 | 243 | active, |
| 244 | renderingActive, | |
| 236 | 245 | cornerRadius: presentation.cornerRadius, |
| 237 | 246 | rect: lastRect, |
| 238 | 247 | hiddenSize, |
| ... | ... | @@ -244,6 +253,7 @@ export function HostedBrowserWebview(props: { |
| 244 | 253 | className="fixed overflow-hidden bg-muted/35" |
| 245 | 254 | style={{ ...wrapperStyle, overscrollBehavior: "contain" }} |
| 246 | 255 | onScroll={syncContentPresentation} |
| 256 | data-preview-rendering={renderingActive ? "active" : "suspended"} | |
| 247 | 257 | data-preview-viewport={runtimeTabId} |
| 248 | 258 | > |
| 249 | 259 | <div className="relative" style={{ width: layout.canvasWidth, height: layout.canvasHeight }}> |
apps/web/src/browser/browserRecording.test.ts+4-4| ... | ... | @@ -179,10 +179,10 @@ describe("browser recording", () => { |
| 179 | 179 | |
| 180 | 180 | it("starts recording for a visible tab", async () => { |
| 181 | 181 | await startBrowserRecording("recording-tab"); |
| 182 | ||
| 183 | expect(events).toEqual(["start-screencast", "publish:recording-tab"]); | |
| 182 | const startupEvents = [...events]; | |
| 184 | 183 | |
| 185 | 184 | await stopBrowserRecording("recording-tab"); |
| 185 | expect(startupEvents).toEqual(["publish:recording-tab", "start-screencast"]); | |
| 186 | 186 | }); |
| 187 | 187 | |
| 188 | 188 | it("records a hidden tab without requiring it to become visible", async () => { |
| ... | ... | @@ -195,11 +195,11 @@ describe("browser recording", () => { |
| 195 | 195 | }; |
| 196 | 196 | |
| 197 | 197 | await startBrowserRecording("recording-tab"); |
| 198 | const startupEvents = [...events]; | |
| 198 | 199 | |
| 199 | 200 | expect(startScreencast).toHaveBeenCalledWith("recording-tab"); |
| 200 | expect(events).toEqual(["start-screencast", "publish:recording-tab"]); | |
| 201 | ||
| 202 | 201 | await stopBrowserRecording("recording-tab"); |
| 202 | expect(startupEvents).toEqual(["publish:recording-tab", "start-screencast"]); | |
| 203 | 203 | }); |
| 204 | 204 | |
| 205 | 205 | it("fails startup instead of locking a fallback size when no frame arrives", async () => { |
apps/web/src/browser/browserRecording.ts+8-6| ... | ... | @@ -122,6 +122,12 @@ export function useActiveBrowserRecordingTabIds(): ReadonlySet<string> { |
| 122 | 122 | const activeRecordings = new Map<string, ActiveRecording>(); |
| 123 | 123 | let unsubscribeFrames: (() => void) | null = null; |
| 124 | 124 | |
| 125 | const publishActiveRecordingTabIds = (): void => { | |
| 126 | appAtomRegistry.set(activeBrowserRecordingTabIdsAtom, { | |
| 127 | tabIds: new Set(activeRecordings.keys()), | |
| 128 | }); | |
| 129 | }; | |
| 130 | ||
| 125 | 131 | export const BROWSER_RECORDING_STARTUP_SETTLE_TIMEOUT_MS = 5_000; |
| 126 | 132 | export const BROWSER_RECORDING_FIRST_FRAME_SIZE_TIMEOUT_MS = 5_000; |
| 127 | 133 | |
| ... | ... | @@ -228,9 +234,7 @@ const clearActiveRecording = (recording: ActiveRecording): void => { |
| 228 | 234 | unsubscribeFrames?.(); |
| 229 | 235 | unsubscribeFrames = null; |
| 230 | 236 | } |
| 231 | appAtomRegistry.set(activeBrowserRecordingTabIdsAtom, { | |
| 232 | tabIds: new Set(activeRecordings.keys()), | |
| 233 | }); | |
| 237 | publishActiveRecordingTabIds(); | |
| 234 | 238 | }; |
| 235 | 239 | |
| 236 | 240 | const cleanupFailedRecordingStart = async ( |
| ... | ... | @@ -377,6 +381,7 @@ export async function startBrowserRecording( |
| 377 | 381 | lifecycle: { phase: "starting" }, |
| 378 | 382 | }; |
| 379 | 383 | activeRecordings.set(tabId, recording); |
| 384 | publishActiveRecordingTabIds(); | |
| 380 | 385 | try { |
| 381 | 386 | try { |
| 382 | 387 | unsubscribeFrames ??= bridge.recording.onFrame(drawFrame); |
| ... | ... | @@ -487,9 +492,6 @@ export async function startBrowserRecording( |
| 487 | 492 | if (recording.lifecycle.phase === "starting") { |
| 488 | 493 | recording.lifecycle = { phase: "recording" }; |
| 489 | 494 | } |
| 490 | appAtomRegistry.set(activeBrowserRecordingTabIdsAtom, { | |
| 491 | tabIds: new Set(activeRecordings.keys()), | |
| 492 | }); | |
| 493 | 495 | return startedAt; |
| 494 | 496 | } finally { |
| 495 | 497 | settleStartup?.(); |
apps/web/src/browser/browserSurfaceStore.test.ts+13-1| ... | ... | @@ -2,13 +2,25 @@ import { beforeEach, describe, expect, it } from "vite-plus/test"; |
| 2 | 2 | |
| 3 | 3 | import { |
| 4 | 4 | acquireBrowserSurface, |
| 5 | acquireBrowserSurfaceActivity, | |
| 5 | 6 | resolveBrowserSurfacePanelRect, |
| 6 | 7 | useBrowserSurfaceStore, |
| 7 | 8 | } from "./browserSurfaceStore"; |
| 8 | 9 | |
| 9 | 10 | describe("browserSurfaceStore", () => { |
| 10 | 11 | beforeEach(() => { |
| 11 | useBrowserSurfaceStore.setState({ byTabId: {} }); | |
| 12 | useBrowserSurfaceStore.setState({ activityByTabId: {}, byTabId: {} }); | |
| 13 | }); | |
| 14 | ||
| 15 | it("keeps concurrent background work active until every lease is released", () => { | |
| 16 | const first = acquireBrowserSurfaceActivity("background-browser"); | |
| 17 | const second = acquireBrowserSurfaceActivity("background-browser"); | |
| 18 | ||
| 19 | first(); | |
| 20 | expect(useBrowserSurfaceStore.getState().activityByTabId["background-browser"]).toBe(1); | |
| 21 | ||
| 22 | second(); | |
| 23 | expect(useBrowserSurfaceStore.getState().activityByTabId["background-browser"]).toBeUndefined(); | |
| 12 | 24 | }); |
| 13 | 25 | |
| 14 | 26 | it("freezes the source content dimensions for a fitted presentation", () => { |
apps/web/src/browser/browserSurfaceStore.ts+26| ... | ... | @@ -29,7 +29,9 @@ export interface BrowserSurfaceContentPresentation { |
| 29 | 29 | } |
| 30 | 30 | |
| 31 | 31 | interface BrowserSurfaceStoreState { |
| 32 | readonly activityByTabId: Record<string, number>; | |
| 32 | 33 | readonly byTabId: Record<string, BrowserSurfacePresentation>; |
| 34 | readonly acquireActivity: (tabId: string) => () => void; | |
| 33 | 35 | readonly claim: (tabId: string, owner: symbol, fitSourceContent: boolean) => void; |
| 34 | 36 | readonly present: ( |
| 35 | 37 | tabId: string, |
| ... | ... | @@ -63,7 +65,28 @@ const rectEquals = (left: BrowserSurfaceRect | null, right: BrowserSurfaceRect): |
| 63 | 65 | left.height === right.height; |
| 64 | 66 | |
| 65 | 67 | export const useBrowserSurfaceStore = create<BrowserSurfaceStoreState>()((set) => ({ |
| 68 | activityByTabId: {}, | |
| 66 | 69 | byTabId: {}, |
| 70 | acquireActivity: (tabId) => { | |
| 71 | let released = false; | |
| 72 | set((state) => ({ | |
| 73 | activityByTabId: { | |
| 74 | ...state.activityByTabId, | |
| 75 | [tabId]: (state.activityByTabId[tabId] ?? 0) + 1, | |
| 76 | }, | |
| 77 | })); | |
| 78 | return () => { | |
| 79 | if (released) return; | |
| 80 | released = true; | |
| 81 | set((state) => { | |
| 82 | const count = state.activityByTabId[tabId] ?? 0; | |
| 83 | const activityByTabId = { ...state.activityByTabId }; | |
| 84 | if (count <= 1) delete activityByTabId[tabId]; | |
| 85 | else activityByTabId[tabId] = count - 1; | |
| 86 | return { activityByTabId }; | |
| 87 | }); | |
| 88 | }; | |
| 89 | }, | |
| 67 | 90 | claim: (tabId, owner, fitSourceContent) => |
| 68 | 91 | set((state) => { |
| 69 | 92 | const current = state.byTabId[tabId]; |
| ... | ... | @@ -171,6 +194,9 @@ export const useBrowserSurfaceStore = create<BrowserSurfaceStoreState>()((set) = |
| 171 | 194 | }), |
| 172 | 195 | })); |
| 173 | 196 | |
| 197 | export const acquireBrowserSurfaceActivity = (tabId: string): (() => void) => | |
| 198 | useBrowserSurfaceStore.getState().acquireActivity(tabId); | |
| 199 | ||
| 174 | 200 | export function acquireBrowserSurface( |
| 175 | 201 | tabId: string, |
| 176 | 202 | fitSourceContent = false, |
apps/web/src/browser/hostedBrowserWebviewStyle.test.ts+23-1| ... | ... | @@ -10,6 +10,7 @@ describe("resolveHostedBrowserWebviewWrapperStyle", () => { |
| 10 | 10 | expect( |
| 11 | 11 | resolveHostedBrowserWebviewWrapperStyle({ |
| 12 | 12 | active: true, |
| 13 | renderingActive: true, | |
| 13 | 14 | rect: { x: 12, y: 34, width: 800, height: 600 }, |
| 14 | 15 | hiddenSize: { width: 1280, height: 800 }, |
| 15 | 16 | }), |
| ... | ... | @@ -27,6 +28,7 @@ describe("resolveHostedBrowserWebviewWrapperStyle", () => { |
| 27 | 28 | expect( |
| 28 | 29 | resolveHostedBrowserWebviewWrapperStyle({ |
| 29 | 30 | active: true, |
| 31 | renderingActive: true, | |
| 30 | 32 | cornerRadius: 12, |
| 31 | 33 | rect: { x: 12, y: 34, width: 360, height: 203 }, |
| 32 | 34 | hiddenSize: { width: 1280, height: 800 }, |
| ... | ... | @@ -40,9 +42,10 @@ describe("resolveHostedBrowserWebviewWrapperStyle", () => { |
| 40 | 42 | }); |
| 41 | 43 | }); |
| 42 | 44 | |
| 43 | it("keeps an inactive webview paintable while moving it offscreen", () => { | |
| 45 | it("suspends painting for an inactive webview", () => { | |
| 44 | 46 | const style = resolveHostedBrowserWebviewWrapperStyle({ |
| 45 | 47 | active: false, |
| 48 | renderingActive: false, | |
| 46 | 49 | rect: { x: 12, y: 34, width: 800, height: 600 }, |
| 47 | 50 | hiddenSize: { width: 393, height: 852 }, |
| 48 | 51 | }); |
| ... | ... | @@ -54,6 +57,25 @@ describe("resolveHostedBrowserWebviewWrapperStyle", () => { |
| 54 | 57 | height: 852, |
| 55 | 58 | zIndex: -1, |
| 56 | 59 | pointerEvents: "none", |
| 60 | visibility: "hidden", | |
| 61 | }); | |
| 62 | }); | |
| 63 | ||
| 64 | it("keeps an active background task paintable offscreen", () => { | |
| 65 | const style = resolveHostedBrowserWebviewWrapperStyle({ | |
| 66 | active: false, | |
| 67 | renderingActive: true, | |
| 68 | rect: null, | |
| 69 | hiddenSize: { width: 1280, height: 800 }, | |
| 70 | }); | |
| 71 | ||
| 72 | expect(style).toEqual({ | |
| 73 | left: HIDDEN_BROWSER_WEBVIEW_OFFSET, | |
| 74 | top: HIDDEN_BROWSER_WEBVIEW_OFFSET, | |
| 75 | width: 1280, | |
| 76 | height: 800, | |
| 77 | zIndex: -1, | |
| 78 | pointerEvents: "none", | |
| 57 | 79 | visibility: "visible", |
| 58 | 80 | }); |
| 59 | 81 | }); |
apps/web/src/browser/hostedBrowserWebviewStyle.ts+4-6| ... | ... | @@ -13,18 +13,19 @@ export interface HostedBrowserWebviewWrapperStyle { |
| 13 | 13 | readonly zIndex: number; |
| 14 | 14 | readonly pointerEvents: "auto" | "none"; |
| 15 | 15 | readonly borderRadius?: number; |
| 16 | readonly visibility?: "visible"; | |
| 16 | readonly visibility?: "hidden" | "visible"; | |
| 17 | 17 | } |
| 18 | 18 | |
| 19 | 19 | export const HIDDEN_BROWSER_WEBVIEW_OFFSET = -100_000; |
| 20 | 20 | |
| 21 | 21 | export function resolveHostedBrowserWebviewWrapperStyle(input: { |
| 22 | 22 | readonly active: boolean; |
| 23 | readonly renderingActive: boolean; | |
| 23 | 24 | readonly cornerRadius?: number; |
| 24 | 25 | readonly rect: BrowserSurfaceRect | null; |
| 25 | 26 | readonly hiddenSize: HostedBrowserWebviewSize; |
| 26 | 27 | }): HostedBrowserWebviewWrapperStyle { |
| 27 | const { active, cornerRadius = 0, hiddenSize, rect } = input; | |
| 28 | const { active, cornerRadius = 0, hiddenSize, rect, renderingActive } = input; | |
| 28 | 29 | if (active && rect) { |
| 29 | 30 | return { |
| 30 | 31 | left: rect.x, |
| ... | ... | @@ -44,9 +45,6 @@ export function resolveHostedBrowserWebviewWrapperStyle(input: { |
| 44 | 45 | height: hiddenSize.height, |
| 45 | 46 | zIndex: -1, |
| 46 | 47 | pointerEvents: "none", |
| 47 | // Keep the guest CSS-visible even while physically offscreen. Electron | |
| 48 | // webviews can keep metadata/status alive under `visibility:hidden` while | |
| 49 | // CDP Runtime/Input commands stall, which breaks offscreen automation. | |
| 50 | visibility: "visible", | |
| 48 | visibility: renderingActive ? "visible" : "hidden", | |
| 51 | 49 | }; |
| 52 | 50 | } |
apps/web/src/components/preview/PreviewAutomationHosts.tsx+20-11| ... | ... | @@ -37,7 +37,10 @@ import { |
| 37 | 37 | stopBrowserRecording, |
| 38 | 38 | } from "~/browser/browserRecording"; |
| 39 | 39 | import { resolveBrowserRecordingStopTarget } from "~/browser/browserRecordingScope"; |
| 40 | import { useBrowserSurfaceStore } from "~/browser/browserSurfaceStore"; | |
| 40 | import { | |
| 41 | acquireBrowserSurfaceActivity, | |
| 42 | useBrowserSurfaceStore, | |
| 43 | } from "~/browser/browserSurfaceStore"; | |
| 41 | 44 | import { browserDefaultOpenViewport, resolveBrowserDefaults } from "~/browser/browserDefaults"; |
| 42 | 45 | import { runBrowserViewportMutation } from "~/browser/browserViewportActions"; |
| 43 | 46 | import { previewRuntimeTabId } from "~/browser/previewRuntimeTabId"; |
| ... | ... | @@ -98,7 +101,7 @@ const waitForDesktopOverlay = async ( |
| 98 | 101 | operation, |
| 99 | 102 | requestId, |
| 100 | 103 | }); |
| 101 | if (state.desktopByTabId[tabId] && previewBridge) { | |
| 104 | if (state.desktopByTabId[tabId] && previewBridge && isPreviewWebviewRendering(runtimeTabId)) { | |
| 102 | 105 | const status = await previewBridge.automation.status(runtimeTabId); |
| 103 | 106 | if (status.available) return; |
| 104 | 107 | } |
| ... | ... | @@ -121,6 +124,11 @@ const findPreviewWebview = (tabId: string): ExecutablePreviewWebview | null => |
| 121 | 124 | (candidate) => candidate.getAttribute("data-preview-tab") === tabId, |
| 122 | 125 | ) ?? null; |
| 123 | 126 | |
| 127 | const isPreviewWebviewRendering = (runtimeTabId: string): boolean => { | |
| 128 | const wrapper = findPreviewWebview(runtimeTabId)?.closest<HTMLElement>("[data-preview-viewport]"); | |
| 129 | return wrapper?.getAttribute("data-preview-rendering") === "active"; | |
| 130 | }; | |
| 131 | ||
| 124 | 132 | const readWebviewViewport = async ( |
| 125 | 133 | webview: ExecutablePreviewWebview, |
| 126 | 134 | ): Promise<PreviewRenderedViewportSize | null> => { |
| ... | ... | @@ -212,8 +220,12 @@ const currentStatus = async ( |
| 212 | 220 | const visible = runtimeTabId |
| 213 | 221 | ? (useBrowserSurfaceStore.getState().byTabId[runtimeTabId]?.visible ?? false) |
| 214 | 222 | : false; |
| 223 | const renderingActive = runtimeTabId ? isPreviewWebviewRendering(runtimeTabId) : false; | |
| 215 | 224 | const viewportSetting = snapshot ? (snapshot.viewport ?? FILL_PREVIEW_VIEWPORT) : undefined; |
| 216 | const viewport = runtimeTabId ? await readRenderedViewport(runtimeTabId).catch(() => null) : null; | |
| 225 | const viewport = | |
| 226 | runtimeTabId && renderingActive | |
| 227 | ? await readRenderedViewport(runtimeTabId).catch(() => null) | |
| 228 | : null; | |
| 217 | 229 | const viewportStatus = { |
| 218 | 230 | ...(viewportSetting === undefined ? {} : { viewportSetting }), |
| 219 | 231 | ...(viewport === null ? {} : { viewport }), |
| ... | ... | @@ -307,6 +319,7 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId }) |
| 307 | 319 | threadId: request.threadId, |
| 308 | 320 | }; |
| 309 | 321 | let tabId = request.tabId ?? null; |
| 322 | const browserActivity = { release: null as (() => void) | null }; | |
| 310 | 323 | try { |
| 311 | 324 | let state = readThreadPreviewState(threadRef); |
| 312 | 325 | const needsSessionSync = needsPreviewAutomationSessionSync(state, request.tabId); |
| ... | ... | @@ -340,6 +353,7 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId }) |
| 340 | 353 | } |
| 341 | 354 | const readyState = readThreadPreviewState(threadRef); |
| 342 | 355 | const runtimeTabId = previewRuntimeTabId(threadRef, readyState.serverEpoch, readyTabId); |
| 356 | browserActivity.release ??= acquireBrowserSurfaceActivity(runtimeTabId); | |
| 343 | 357 | await waitForDesktopOverlay( |
| 344 | 358 | threadRef, |
| 345 | 359 | request.requestId, |
| ... | ... | @@ -440,14 +454,7 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId }) |
| 440 | 454 | usePreviewMiniPlayerStore.getState().open(threadRef, activeTabId); |
| 441 | 455 | } |
| 442 | 456 | if (activeSnapshot && previewAutomationOpenNeedsOverlay(input, activeSnapshot)) { |
| 443 | await waitForDesktopOverlay( | |
| 444 | threadRef, | |
| 445 | request.requestId, | |
| 446 | activeTabId, | |
| 447 | activeRuntimeTabId, | |
| 448 | request.operation, | |
| 449 | request.timeoutMs, | |
| 450 | ); | |
| 457 | await requireReadyTab(); | |
| 451 | 458 | } |
| 452 | 459 | if (shouldPresentPreview) { |
| 453 | 460 | // React commits the thread-bound surface asynchronously. Settle |
| ... | ... | @@ -678,6 +685,8 @@ function PreviewAutomationHost(props: { readonly environmentId: EnvironmentId }) |
| 678 | 685 | tabId, |
| 679 | 686 | cause, |
| 680 | 687 | }); |
| 688 | } finally { | |
| 689 | browserActivity.release?.(); | |
| 681 | 690 | } |
| 682 | 691 | }, |
| 683 | 692 | [environmentId, listPreviews, open, registry, resize], |