Repository navigation
fix(desktop): a hidden preview tab no longer keeps the display awake after a screenshot - #25978
juliusmarminge wants to merge 4 commits into
Conversation
…ore Chromium answers These tests reproduce #17870. A Page.captureScreenshot already sent to Chromium must keep the guest painting until Chromium answers it, even after the agent's turn ends or the relay is released. Otherwise the client parks the guest offscreen, the capture never gets a frame, and Chromium's "Capturing" display wake lock stays held. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…um answers it Chromium holds a "Capturing" display wake lock for each Page.captureScreenshot until a painted frame answers it. The host stopped painting a guest when the agent's turn ended, the relay was released, or the session detached, even with a screenshot already sent, so the guest went offscreen and the lock leaked until the renderer died, keeping the Mac awake for hours. Painting now follows a per-tab set of screenshots sent to the debugger and not yet answered, plus any live queued screenshot. A new document still clears it and detaching still stops painting. Focus emulation is unchanged. Fixes #17870 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ting Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — This is a focused desktop bug fix that separates screenshot capture lifetime from focus lifetime, preventing hidden guests from stopping painting before Chromium answers. The production change is isolated to preview capture lifecycle handling and is accompanied by targeted regression coverage. You can add or adjust custom eligibility rules. Learn more. |
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Track capture painting state apps/desktop/src/preview/DesktopBrowserHost.ts |
A capturing set tracks screenshot requests awaiting responses. Painting synchronization includes active screencasts, pending captures, and live waiting screenshots. Capture requests with a current predicate are wrapped so painting is synchronized until the send settles. |
Release focus holds and verify capture behavior apps/desktop/src/preview/DesktopBrowserHost.ts, apps/desktop/src/preview/DesktopBrowserHost.test.ts |
Turn end and session cleanup release focus holds independently of pending capture painting. Tests cover pending captures across viewer detach, navigation, relay release, and turn end. |
Priority:
Estimated code review effort: 3 (Moderate) | ~15 minutes
Change: Bug fix
Possibly related issues
- Issue 23936 reports
preview_snapshottimeouts for hidden and delegated tabs. This change keeps a guest painted until its screenshot request receives an answer. - Issue 24514 reports unanswered
Page.captureScreenshotrequests for off-window guests. This change tracks painting through the pending request. - Issue 17483 reports preview automation stalls when the browser panel moves off-screen. This change addresses the screenshot-related painting behavior.
- Issue 23514 reports an unanswered capture on a hidden guest blocking later actions. This change keeps the guest painted while a sent capture is pending.
Possibly related PRs
- pingdotgg/t3code#24806: Introduced painting state for pending screenshots and active screencasts in
DesktopBrowserHost. - pingdotgg/t3code#25184: Implements the hidden-preview focus and painting model that this change extends.
- pingdotgg/t3code#24056: Serializes screenshot requests and releases the active screenshot turn on navigation or relay release.
Suggested reviewers: t3dotgg, macroscopeapp
Merge Risk: 
Minimal · up to 0f521
-
The change appears ready to merge after normal checks; no concrete outstanding failure is established.
Pre-merge checks: 
2 · 
1 · 
1
-
Failed checks: 1 warning · 1 inconclusive Passed checks: 2 passed-
Full details: Out of Scope Changes check
-
Explanation
Every change in the PR serves the objective of keeping the guest painted until a sent screenshot is answered.
holdFocusno longer receivesmethodor setspaints. Focus hold behavior is kept, and painting moves tocapturing.paintUntilAnsweredwraps thePage.captureScreenshotsend only. It runs only whencurrent()is true.syncPaintingruns onPage.startScreencastandPage.stopScreencast. This keeps painting consistent after the removal of theholdFocussync. The screencast set still counts toward painting, so the behavior is unchanged.endAgentTurnanddropFocusHoldsnow end focus holds only. This is the turn-end behavior the issue describes.onNewDocumentclearscapturing. This follows the existing rule that a new document answers nothing pending from the old one.- Comment and type updates in
AttachedTaband related helpers match these changes.
The test file changes, taken from the raw summary and not inspected directly, add the two regression tests and update four expectations that encoded the old focus and painting rule. These support the same objective. No unrelated file in the preview directory is changed by this PR.
-
Full details: Description check
-
Explanation
The description follows the template's main sections: Problem, Change, Scope and approval, and Verification. The Problem and Change sections are specific. They name the affected component (
DesktopBrowserHost), the stalledPage.captureScreenshotpath, and the wake-lock symptom. Verification is thorough. It lists red-first regression tests, test counts, and a native Electron reproduction with log excerpts. It also states what was not checked (macOSpmset). The main gap is the Scope and approval section. It contains only "Fixes #17870". The template asks for an approval link or explicit justification for a small fix. The linked issue is referenced, but the description does not state whether a maintainer approved the direction and scope, or explain why the fix qualifies without that approval. The Verification section also depends heavily on external file-drop links, and the evidence bundle is very large relative to the code change. The reviewer cannot easily verify most of it. The template also asks for a conventional commit title, which the PR description does not show. The title is outside the description body, so it could not be assessed here.Resolution
- Scope and approval: state whether a maintainer approved the direction and scope of the fix. Link the triaged issue
#17870and the approval comment, or name the maintainer decision. If no approval exists, explain why this is a very small, focused fix of an obvious bug. Do not rely on the issue link alone. 2. Verification: keep the in-description summary of the focused test results and the reproduction. Move the bulk of the evidence links into a short summary, and confirm that the linked artifacts remain accessible. Do not depend on external file links for the core proof. 3. Confirm the PR title uses a conventional commit format in plain language, as the template requires. 4. Clarify the Problem section in one or two sentences, as the template asks, and separate the wake-lock symptom from the reproduction details.
- Scope and approval: state whether a maintainer approved the direction and scope of the fix. Link the triaged issue
Full details: Linked Issues check
-
Explanation
Issue
#17870is the only active direct link. Its symptom is a "Capturing" wake lock that stays held on macOS. Its diagnosis is a screencast teardown leak, and it lists suggested fixes in apps/desktop/src/preview/DesktopBrowserHost.ts (detach, release, announceAll), apps/server/src/preview/ServerBrowser.ts (dropTab) and apps/desktop/src/preview/Manager.ts (screencast ack). The issue states that this diagnosis was not reproduced.What the PR changes at head 0f52106 (diff against merge base 36d4d71, DesktopBrowserHost.ts): a per-tab
capturingset.paintUntilAnsweredadds eachPage.captureScreenshotentry when it is sent and removes it when the send settles.syncPaintingkeeps the guest painted while that set is non-empty.endAgentTurnanddropFocusHoldsno longer end painting.onNewDocumentclears the set. The change does not stop screencasts on relay release, detach, or in dropTab. It does not change ServerBrowser.ts or Manager.ts.The PR reports a Linux/Xvfb Electron 44.9.3 reproduction. It shows that an unanswered capture held offscreen keeps
isBeingCaptured() === true, and that painting the guest answers it and releases the capturer. The PR also states that on Electron 44Page.startScreencasttakes no capturer. That statement contradicts the issue's diagnosis. The native transcript is linked externally and was not inspected. The macOSpmset -g assertionscheck was not performed.The repository evidence supports a fix for the sent-but-unanswered screenshot path. It does not establish that this path is the cause of the reported macOS lock. The issue's suggested screencast teardown fixes are not implemented, and the PR does not state why they are unnecessary beyond the Electron 44 claim.
Resolution
Provide one of the following so the requirement can be decided:
- Evidence from macOS that the "Capturing" assertion is released after this change. For example,
pmset -g assertionsoutput before and after, or a comparable run on the reported build. - Evidence that on the affected Electron version, a held "Capturing" assertion is caused by an unanswered
Page.captureScreenshotand not byPage.startScreencast. The linked native transcript must be reviewable in the PR, or its key results must be stated in the PR text. - An explicit statement from the maintainers that the issue's screencast teardown suggestions (stop screencasts on relay release, detach, and in dropTab) are out of scope for
#17870. Alternatively, the PR should address them.
- Evidence from macOS that the "Capturing" assertion is released after this change. For example,
-
Finishing Touches
-
📝 Generate docstrings
-
- Commit to this branch
- Create a new PR
🧪 Generate unit tests (beta)
-
- Commit to this branch
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Comment @coderabbitai help to get the list of available commands.
|
Taking over this one |
|
Taking over this one |
Problem
The desktop app can hold Chromium's "Capturing" display wake lock for hours, so a Mac never idle-sleeps (
pmset -g assertionsshows aNoDisplaySleepAssertionnamed "Capturing" from T3 Code). That lock is thestay_awakecapturer Chromium takes for a CDPPage.captureScreenshotand keeps until the screenshot answers. A screenshot sent to a preview guest parked offscreen never gets a frame, so it never answers.DesktopBrowserHostpaints a hidden guest behind the app while a screenshot is pending, but stopped painting as soon as T3 stopped waiting: at the agent's turn end, on a relay release or backend reconnect, or when the viewer that asked detached. The capture Chromium had already been sent then stayed pending offscreen and kept the lock until the page loaded a new document or the tab closed.Reproduced on Electron 44.9.3 (the shipped version) under Xvfb with a guest parked exactly where T3 parks a non-painting one (
position: fixedat -100000px). The capture stays pending withwebContents.isBeingCaptured() === truefor as long as the guest is offscreen. Moving it into the viewport behind the app answers it about 50 ms later and releases the capturer. On Electron 44,Page.startScreencasttakes no capturer, so the screencast paths named in the issue are not what holds the lock.Change
Painting now follows the screenshots Chromium has actually been sent and not yet answered: a per-tab set entered at the debugger send and left when that send settles. The guest stays painted until Chromium answers, so the capturer and its wake lock are released, regardless of turn end, relay release or session detach. A new document still clears the set, and a detached tab still stops painting. Focus-emulation timing is unchanged; it takes no wake lock (
stay_awake=false).Scope and approval
Fixes #17870
Verification
Red first:
DesktopBrowserHost.test.tsgained "paints for a screenshot already sent until Chromium answers it, past the turn" and "... past a release". Both fail on main (expected [ true, false ] to deeply equal [ true ]) and pass with the fix. Four existing expectations that asserted the old drop-on-turn-end/release rule were updated. The navigate and detach parts keep their behaviour.vp test run src/preview/DesktopBrowserHost.test.ts: 34/34 passed.DesktopBrowserHost,ManagerandCdpRelaytests together: 97 passed.vp run check:changed: format, lint, knip, typecheck and tests pass.Native runtime transcript (Electron 44.9.3, Linux/Xvfb): electron-hidden-guest-capture-harness.js, log at T3's -100000 offset, log at -10000:
No before/after captures: the change has no visible surface. The guest it keeps painted sits behind the app at
z-index: -1.cannot-verify macos:pmset -g assertionswas not observed on a Mac. The capturer and the offscreen stall come from shared Chromium code, reproduced on Linux.Model and harness: Claude Opus 5.5 (Claude Code) as issue lead, red test and implementation. Reviews by GPT-6.1 Sol (Codex) and Claude Opus 5.5 (Claude Code).
🤖 Generated with Claude Code
Takeover
No code changes; evidence and verification added below.
Evidence
Captured 16/17 matrix rows, including 10/10 core recordings (C/T/R1/R2/D × before/after). Linux native capturer evidence only; no macOS assertion claim.
Linux fs-35; headless-desktop/Xvfb 1920×1080×24; real Electron 44.9.3 / Chromium 152.0.7977.167; app 1440×1000, DPR 1, dark; --no-sandbox --disable-gpu. Core guest 1280×800; V and its subsequent E control use the app’s 539×920 visible/stored viewport on both revisions. ImageMagick import + ffmpeg x11grab at 10 fps. Renderer/fixture Date frozen at 2026-10-27T12:00:00Z; UTC/native monotonic clocks run normally. One width/theme because this host change has no theme or breakpoint branch.
Steps: C takes two ordinary snapshots, then verifies the window-hide frame gate. For T/R1/R2/D: hide the real app window → native screenshot send → lifecycle action → actual F=false → restore. Before: observe a bounded 2-second restored-window stall, then label CSS rescue. After: native completion on restore, then P− and B=false. Per-row steps/timestamps and excerpts; timing validation / control validation / derived relay generations.
Synchronized provenance pair (in-row markers; timestamps/chapters): before-all-rows.mp4 · after-all-rows.mp4 · recording-chapters.json.
What else changed: identical 72-line scratch instrumentation in six files; main/service controllers, 500 ms B sampler, renderer-style observer, synthetic seed/HTML, fixed Date and row markers. Native screenshot completion probe is below every gate. F1/F0 defer only native-focus promise delivery and prove host ordering, not a screenshot wake lock. R2 additionally blocks renderer WebSocket reconnect to preserve the guest, then restores its synthetic in-memory tab registry after native completion to verify a new-backend PNG. T uses an actual running Cursor auto provider turn and authenticated run.interrupt; D closes the actual read-only viewer socket owning the pending JPEG.
Idle C stills: zero changed pixels, both exact and channel threshold 8/255; both diffs inspected. Before:
After: 

Scripts/seeds: drivers-seeds-and-instrumentation.tar.gz; capture-real-turn.py; capture-lifecycle.py; capture-reconnect.py. Patches: instrumentation-before.patch / instrumentation-after.patch; instrumentation-equivalence.json. Raw source logs/recordings: raw-runtime-and-recordings.tar.gz.
Unverified: M (
cannot-verify macos). Additional variants not claimed: F0 owner-detach/new-document before-send cases, X same-key guest replacement, N queued-successor variant. No tests substitute for these captures. Native/host logbytesdenotes encoded data length; service-driver byte counts are decoded PNG lengths where logged.Verification log
mainat 36d4d71. No code, test or review changes this round.native_send < lifecycle_received < native_settleon T, R1, R2 and D on both sides. It also confirmed the two instrumentation patches add the same 72 lines and leave the painting decision unchanged. It re-derived D from the raw archive, inspected 85 recording frames, and matched all checksums.P-at turn end while the screenshot is pending, and B stays true for 2 s after the window is restored, until the labelled CSS rescue. After: noP-at turn end, the screenshot answers 140 ms after restore, thenP-and B=false.DesktopBrowserHost.test.ts34/34;DesktopBrowserHost,ManagerandCdpRelaytogether 97 passed.cannot-verify macos.pmset -g assertionswas not observed on a Mac.