Skip to content

fix(desktop): a hidden preview tab no longer keeps the display awake after a screenshot - #25978

Open
juliusmarminge wants to merge 4 commits into
mainfrom
fix/issue-17870
Open

juliusmarminge wants to merge 4 commits into
mainfrom
fix/issue-17870

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Problem

The desktop app can hold Chromium's "Capturing" display wake lock for hours, so a Mac never idle-sleeps (pmset -g assertions shows a NoDisplaySleepAssertion named "Capturing" from T3 Code). That lock is the stay_awake capturer Chromium takes for a CDP Page.captureScreenshot and keeps until the screenshot answers. A screenshot sent to a preview guest parked offscreen never gets a frame, so it never answers.

DesktopBrowserHost paints 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: fixed at -100000px). The capture stays pending with webContents.isBeingCaptured() === true for 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.startScreencast takes 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.ts gained "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, Manager and CdpRelay tests 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:

    [1.80s] guest offscreen and loaded; isBeingCaptured false
    [2.81s] offscreen: pending true isBeingCaptured true
    [9.82s] offscreen: pending true isBeingCaptured true
    [9.82s] painting: moving guest into the viewport behind the app (left/top 0, z-index -1)
    [9.87s] capture ANSWERED after 8.06 s, base64 bytes 6960
    [10.33s] painted: pending false isBeingCaptured false
    
  • 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 assertions was 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.

Row level Before 36d4d711b7 After 0f5210654a verdict
I host-command control before-I.jsonl after-I.jsonl idle: no P+, no screenshot; B=false
C real app, service driver before-C.jsonl after-C.jsonl ordinary PNGs + verified native frame gate complete on both
T gated real app before-T.jsonl after-T.jsonl actual Cursor run.interrupt: before stalls; after releases
R1 instrumented real app before-R1.jsonl after-R1.jsonl actual endpoint scope close: before stalls; after releases
R2 instrumented real app before-R2.jsonl after-R2.jsonl actual backend restart; survivor + new backend PNG succeed
D gated real app before-D.jsonl after-D.jsonl actual viewer owner detaches during JPEG; before stalls, after releases
QT gated host-command harness before-QT.jsonl after-QT.jsonl two native captures serialize; after keeps painting through handoff
QD gated host-command harness before-QD.jsonl after-QD.jsonl detached queued owner never sends; after retains first
QR gated host-command harness before-QR.jsonl after-QR.jsonl old queued capture never sends; new relay capture succeeds
N gated real app, service driver before-N.jsonl after-N.jsonl reload: old promise unresolved but B=false; successor succeeds
X gated real app before-X.jsonl after-X.jsonl actual preview.close RPC; native guest destroyed (B unavailable)
F1 focus-delivery harness before-F1.jsonl after-F1.jsonl before P+ while unsent; after P+ only at native send
F0 focus-delivery harness before-F0.jsonl after-F0.jsonl relay-release subcase: zero native screenshots; no after P+
E raw host-command harness before-E.jsonl after-E.jsonl actual invalid-format rejection; valid PNG successor; B=false
S raw host-command harness before-S.jsonl after-S.jsonl 2→1→0 streams; sole-page + child refusals preserve counts
V gated visible-panel control before-V.jsonl after-V.jsonl independent visible panel rescues before; both complete
M unavailable unverified unverified cannot-verify macos: Linux VM; no pmset assertions measured

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: before-C-idle.png After: after-C-idle.png
C-diff-zero.png C-diff-threshold-8.png

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 log bytes denotes encoded data length; service-driver byte counts are decoded PNG lengths where logged.

Verification log
  • Round 2, evidence only, head 0f52106, base main at 36d4d71. No code, test or review changes this round.
  • Proof audit pass 1 derived the 17-row fixture matrix (matrix.md): C, T, R1, R2 and D before and after are the core set. Pass 2 is SUFFICIENT. It covered all 16 capturable rows and checked native_send < lifecycle_received < native_settle on 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.
  • The lead also re-derived T from the raw transcripts. Before: 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: no P- at turn end, the screenshot answers 140 ms after restore, then P- and B=false.
  • Tests (from the issue round): DesktopBrowserHost.test.ts 34/34; DesktopBrowserHost, Manager and CdpRelay together 97 passed.
  • CI: all 40 checks green on 0f52106.
  • Unverified: cannot-verify macos. pmset -g assertions was not observed on a Mac.
  • Model and harness: Claude Opus 5.5 (Claude Code) as PR lead; GPT-6.1 Sol (Codex) for the proof audits and evidence capture.

juliusmarminge and others added 4 commits October 27, 2026 14:05
…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>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Oct 27, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 0f52106

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.

@coderabbitai

coderabbitai Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 36d4d71 and 0f52106.

Files selected for processing: 2
  • apps/desktop/src/preview/DesktopBrowserHost.test.ts
  • apps/desktop/src/preview/DesktopBrowserHost.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.

Walkthrough

DesktopBrowserHost now tracks unanswered screenshot captures separately from focus holds. Painting remains active while a sent capture is pending, including after turn end or relay release, and stops when the capture settles or the document changes.

Changes: Screenshot capture lifecycle

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: Normal

Estimated code review effort: 3 (Moderate) | ~15 minutes

Change: Bug fix

Possibly related issues

  • Issue 23936 reports preview_snapshot timeouts for hidden and delegated tabs. This change keeps a guest painted until its screenshot request receives an answer.
  • Issue 24514 reports unanswered Page.captureScreenshot requests 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: Passed 2 · Failed 1 · Inconclusive 1

Failed Failed checks: 1 warning · 1 inconclusive

Check name Status Explanation Resolution
Description check Warning 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 (`DesktopBro… 1. Scope and approval: state whether a maintainer approved the direction and scope of the fix. Link the triaged issue #17870 and the approval comment, or name the maintainer decision. If no approval exists, explain why this is a very small,…
Linked Issues check Inconclusive Issue #17870 is 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/… Provide one of the following so the requirement can be decided:
1. Evidence from macOS that the "Capturing" assertion is released after this change. For example, pmset -g assertions output before and after, or a comparable run on the repo…
Passed Passed checks: 2 passed
Check name Status Explanation
Title check Passed The title clearly identifies the desktop preview wake-lock fix caused by a hidden preview tab after a screenshot. It is concise, specific, and uses a conventional commit prefix.
Out of Scope Changes check Passed Every change in the PR serves the objective of keeping the guest painted until a sent screenshot is answered.
- holdFocus no longer receives method or sets paints. Focus hold behavior is kept, a…
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.

  • holdFocus no longer receives method or sets paints. Focus hold behavior is kept, and painting moves to capturing.
  • paintUntilAnswered wraps the Page.captureScreenshot send only. It runs only when current() is true.
  • syncPainting runs on Page.startScreencast and Page.stopScreencast. This keeps painting consistent after the removal of the holdFocus sync. The screencast set still counts toward painting, so the behavior is unchanged.
  • endAgentTurn and dropFocusHolds now end focus holds only. This is the turn-end behavior the issue describes.
  • onNewDocument clears capturing. This follows the existing rule that a new document answers nothing pending from the old one.
  • Comment and type updates in AttachedTab and 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 stalled Page.captureScreenshot path, 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 (macOS pmset). 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

  1. Scope and approval: state whether a maintainer approved the direction and scope of the fix. Link the triaged issue #17870 and 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.
Full details: Linked Issues check

Explanation

Issue #17870 is 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 capturing set. paintUntilAnswered adds each Page.captureScreenshot entry when it is sent and removes it when the send settles. syncPainting keeps the guest painted while that set is non-empty. endAgentTurn and dropFocusHolds no longer end painting. onNewDocument clears 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 44 Page.startScreencast takes no capturer. That statement contradicts the issue's diagnosis. The native transcript is linked externally and was not inspected. The macOS pmset -g assertions check 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:

  1. Evidence from macOS that the "Capturing" assertion is released after this change. For example, pmset -g assertions output before and after, or a comparable run on the reported build.
  2. Evidence that on the affected Electron version, a held "Capturing" assertion is caused by an unanswered Page.captureScreenshot and not by Page.startScreencast. The linked native transcript must be reviewable in the PR, or its key results must be stated in the PR text.
  3. 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.
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.

@juliusmarminge juliusmarminge added bugfix Fix PR opened by an issue lead; enters the PR loop pr:gate-failed A land gate failed pr:in-progress A lead is working on this PR and removed pr:gate-failed A land gate failed labels Oct 27, 2026

Copy link
Copy Markdown
Member Author

Taking over this one

@juliusmarminge juliusmarminge added pr:gate-failed A land gate failed pr:in-progress A lead is working on this PR and removed pr:in-progress A lead is working on this PR pr:gate-failed A land gate failed labels Oct 27, 2026
@juliusmarminge juliusmarminge self-assigned this Oct 27, 2026

Copy link
Copy Markdown
Member Author

Taking over this one

@juliusmarminge juliusmarminge added pr:needs-verification Reviewed and green; needs a run on a desktop, Android or iOS device and captures posted and removed pr:in-progress A lead is working on this PR labels Oct 27, 2026

This branch has not been deployed

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

Labels

bugfix Fix PR opened by an issue lead; enters the PR loop pr:needs-verification Reviewed and green; needs a run on a desktop, Android or iOS device and captures posted size:M 30-99 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.

macOS desktop: preview screencast leaves "Capturing" wake lock held, Mac never idle-sleeps

1 participant