Repository navigation
fix(browser): capture the tab that was asked for, not the park window - #317
Merged
Merged
Conversation
screenshot_tab intermittently returned another tab's pixels, or a frozen older frame, while get_tab_url / get_tab_clickables stayed correct for the same id in the same moment. Both paths resolve the tab the same way, so the bug was never in resolution — it was in the capture. The offscreen path asked CDP for `Page.captureScreenshot` with `fromSurface: false`. That routes through GetSnapshotFromBrowser, which on macOS grabs the *native window* and crops it to the view's rect. Every hidden tab is parked in one shared park window, so the crop returns whatever composites on top there — a sibling tab's live page, or a stale window frame. `fromSurface: true` copies the requesting view's own compositor surface instead, which is also what the on-screen path has always used. Captures are now serialized too: capturing a visible tab whose emulated viewport is taller than its pane detaches the view and parks it mid-shot, so two captures at once meant one reparenting the other's host. Every await inside a capture is bounded so a wedged one can't stall the queue. Measured in a real Electron run driving BrowserManager directly: one tab visible under a tall emulated viewport, a parked sibling, captured concurrently — the sibling came back holding the other tab's page 3/3 times before, 0/3 after, and a 20-assertion sweep of the reported sequence (two then three tabs, pane switches, mobile emulation, about:blank, hidden host window) goes from 2 failures to 0. Surface capture converts through the display profile, so parked-tab screenshots now have the same slight color shift on-screen captures already had. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
screenshot_tabintermittently returned another tab's pixels, or a frozen older frame, whileget_tab_url/get_tab_clickables/get_tab_console_logsstayed live and correct for the sametab_idin the same moment.Both paths resolve the tab through the same
instances.get(tabId)lookup, so resolution was never the problem — the bug was in the capture.Root cause
captureOffscreenasked CDP forPage.captureScreenshotwithfromSurface: false. That flag does not mean "render in the renderer process" as the old comment claimed — it routes throughRenderWidgetHostImpl::GetSnapshotFromBrowser, which on macOS grabs the native window and crops it to the view's rect.Every hidden tab is parked in one shared park window, so that crop returns whatever composites on top there: a sibling tab's live page, or a stale window frame. One cause, all three reported symptoms — wrong tab, frozen frame, other tools unaffected.
Repro
Two intermediate theories were disproved first (stacked-view z-order alone is harmless; in-page repaints are not stale). The real
BrowserManagerwas then driven directly under Electron 41. Deterministic trigger:A 20-assertion sweep of the reported sequence (two then three tabs, pane switches, mobile emulation,
about:blank, hidden host window, concurrent captures) went from 2 failures to 0.The fix
src/main/browser-manager.ts:fromSurface: true— copies the requesting view's own compositor surface, which is what the on-screen path (capturePage()) has always used. A failure now throws → a 500 the agent can act on, instead of silently handing back someone else's pixels.withCaptureTimeout), so serialization can't let one wedged capture stall every later screenshot.Regression test
src/main/browser-manager-capture.test.tsmocks Electron with fakes that model the measured Chromium semantics — a window-level grab returns the topmost sibling, surface capture returns the view's own frame — so a revert fails the test rather than shipping. Both halves of the fix are independently guarded: revertingfromSurfacefails 2 tests, removing serialization fails 1.Verification
npm run typecheck— cleannpx electron-vite build— cleannpx vitest run— 129 files / 1722 tests passBehavior change worth knowing
Surface capture converts through the display color profile, so parked-tab screenshots now carry the same slight color shift on-screen captures always had (pure
#00ff00reads as ~rgb(117,251,76)). Content is correct; saturated colors are slightly washed. Both paths are consistent now instead of differing by tab state.🤖 Generated with Claude Code