Skip to content

cmux-next: agents can screenshot the app again; browser pages are blacked out - #23983

Merged
teamleaderleo merged 5 commits into
feat-cmux-nextfrom
cmux/window-screenshot-agents
Oct 22, 2026
Merged

teamleaderleo merged 5 commits into
feat-cmux-nextfrom
cmux/window-screenshot-agents

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Leo-ask: ask-49f4ca2104

Takes back part of 45fe246 ("fix(security): refuse unguarded app screenshots", lawrencecchen, 2026-10-20): it made window.screenshot, pane.screenshot and tab.screenshot person-only, so an agent had no way to see a terminal or a chat window. Leo: "cmux only lets a person run the window screenshot action. doesnt seem right". This PR keeps that fix's guarantee: no unmasked browser pixels reach automation.

Summary

  • An automation run (CLI, MCP, scripts) of cmux window|pane|tab screenshot runs again. A person's run (palette, menu) is unchanged.
  • What an automation image hides (ScreenshotPrivacyMask): these areas are painted opaque black, because the app can't reproduce the browser host's secret mask (vault, TOTP, typed secrets):
    • every pane showing a browser tab (the whole browser view, toolbar included);
    • every WKWebView not held by the app's own agent chat (AgentPaneView) or page tabs (InternalPageView), so an unlisted web view fails closed;
    • every Chromium page child window.
  • What it still shows: the app's own React pages on Chromium are painted back. The overlay panels and the app's other windows are left out.
  • Refused for automation: a browser tab, or a pane showing one. The refusal points at the browser REPL's page.screenshot(), which masks secrets.
  • The cmux and cmux-browser skills now say what an agent's screenshot shows.

Risk

risk:high: this changes a security policy (a teammate's fail-closed screenshot gate). It takes back part of a commit pushed straight to feat-cmux-next, which has no PR number.

Changelog

Fixed: Agents can screenshot cmux windows, panes and terminal tabs again; browser pages in the image are blacked out.

Testing

Test first:

  • ActionOriginTests/appScreenshotsRunForEveryCaller: CLI, MCP, script and no-origin runs reach the handler as non-person runs.
  • ScreenshotPrivacyMaskTests:
    • a foreign web view is masked, including when nested;
    • the app's page view is not masked;
    • a hidden web view is not masked;
    • the mask layer paints opaque black.

Observed / Not run

  • Review: an independent review of 238d980 failed on three points, all fixed there:
    • an app's user-origin run (during a person's gesture) was unmasked;
    • only direct Chromium child windows were masked;
    • the masks were measured after the capture.
      A re-review of 238d980 passed (SHIP). A review of the follow-up 91e026a, where a masked capture whose masks cannot be painted now throws instead of sending the unmasked image, also passed (SHIP).
  • Fleet app compile: passed at 238d980 (job 04bdb0f38d2c, 80 s) and at the head 91e026a (job 7a50e9fb5e1e, 86 s).
  • Red and green test runs: package-test-lane ActionOriginTests,ScreenshotPrivacyMaskTests on scratch refs (the red tests, then the fix, each with ci: the agent pane page ships the Inspector's ContextSection.css (base red since #23789) #23935's build-script patch for the base ContextSection.css red). Queued on the fleet; results to be posted as a comment.
  • PR reds that come from base: cmux-next crash-class rules (an objc-selector probe on CEFFileDragRelay.swift, which cmux NEXT browser: files dragged in from Finder drop into the Chromium page #23790 added) and the Checks job structure step of cmux-next checks. This PR touches neither.
  • Not run: a live capture of a window with a browser and a terminal side by side; no test covers nested Chromium child windows.
  • Follow-ups from the review, not in this PR: a resize during the capture can shift the masks; the Passwords page (sites and usernames) is not masked; the web-view scan starts at contentView.
  • No capture: this changes what automation receives, not the app's UI.

Agent session: cosmic-gray

🤖 Generated with Claude Code

teamleaderleo and others added 2 commits October 21, 2026 17:49
…nted black

45fe246 made window, pane and tab screenshots person-only because the app's
window capture cannot reproduce the browser host's secret mask. That left an
agent with no way to see a terminal or a chat (Leo: "cmux only lets a person
run the window screenshot action. doesnt seem right").

An automation run now gets the image with every browser tab area, every web
view the app does not own and every Chromium page window painted opaque black
(ScreenshotPrivacyMask), and without the overlay panels and other windows. A
browser tab, or a pane showing one, is refused and points at the browser REPL.
A person's run is unchanged.

Leo-ask: ask-49f4ca2104

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15bbf-ec34-7473-9030-a90482979ddc&host=cosmic-gray
@teamleaderleo teamleaderleo added the risk:high Risk tier (docs/ci/risk-tiers.md) label Oct 22, 2026
@teamleaderleo
teamleaderleo temporarily deployed to cmux-tui-merge-tree October 22, 2026 01:21 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 91e026a: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr23983 -f ref=cmux/window-screenshot-agents -f platform=macos -f backend_mode=local

red: cmux-next checks (god files, concurrency, crash safety, l10n); NEW: red check: Checks job structure is not red on the base 76db10b; do not merge until green; see lane-rules

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by cmux-mcp-surface on cosmic-gray · session

@github-actions github-actions Bot added risk:normal Risk tier (docs/ci/risk-tiers.md) and removed risk:high Risk tier (docs/ci/risk-tiers.md) labels Oct 22, 2026
@blacksmith-sh

blacksmith-sh Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Found 16 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmp0f_o65wo/summary-fail.json (live):/summary-fail.json (live): View Logs
/tmp/tmpvcbfbxir/summary-missing.json (live):/summary-missing.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs
cmux-next-ios-kit.yml runs macOS jobs on pull requests but a new push does not cancel t
he previous run; key the concurrency group on the pull request and set cancel-in-progre
ss for its pull request events/
cmux-next-ios-kit.yml runs macOS jobs on pull requests but a new push does not cancel t
he previous run; key the concurrency group on the pull request and set cancel-in-progre
ss for its pull request events
View Logs
convention violations found (ERROR lines above)./
convention violations found (ERROR lines above).
View Logs
nightly cmux-tui resolve/nightly cmux-tui resolve View Logs
test_every_submodule_free_checkout_on_an_owned_runner_resets_submodules (main.Reuse
dWorkspaceSubmodules.test_every_submodule_free_checkout_on_an_owned_runner_resets_submo
dules)/
test_every_submodule_free_checkout_on_an_owned_runner_resets_submodules (main.Reuse
dWorkspaceSubmodules.test_every_submodule_free_checkout_on_an_owned_runner_resets_submo
dules)
View Logs

...and 6 more test failures. View all on Blacksmith: run 39711313842, run 39711313898

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

cmux-next crash-class rules / cmux-next crash-class rules is red on 11 of 11 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

teamleaderleo and others added 2 commits October 21, 2026 18:40
…ure, and nested page windows

An app's run carries origin user during the person's gesture in it, so the
mask now applies to every run but a person's own (no callerApp, agent chat or
terminal). The mask walks visibleChildWindows, the same windows the capture
includes, and is measured before the capture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15bbf-ec34-7473-9030-a90482979ddc&host=cosmic-gray
@teamleaderleo
teamleaderleo temporarily deployed to cmux-tui-merge-tree October 22, 2026 02:25 — with GitHub Actions Inactive
@teamleaderleo teamleaderleo added risk:high Risk tier (docs/ci/risk-tiers.md) and removed risk:normal Risk tier (docs/ci/risk-tiers.md) labels Oct 22, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (subagent, correctness first): SHIP at 91e026a

  • Full re-review of 238d980: SHIP. The three earlier blockers are fixed: an app's user-origin run, nested page windows, and masks measured before the capture. It found no path that gives a non-person caller an unmasked image.
  • Delta review of 91e026a (a masked capture whose masks cannot be painted throws WindowScreenshotFailure.blank instead of returning the unmasked image): SHIP.
  • Fleet app compile at 91e026a: passed (job 7a50e9fb5e1e, 86 s).
  • Both reviews were by reading; nothing was built or run.

@github-actions github-actions Bot added risk:normal Risk tier (docs/ci/risk-tiers.md) and removed risk:high Risk tier (docs/ci/risk-tiers.md) labels Oct 22, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Bead cx-2dfdf created for this PR by the cmux coordinator.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Red/green on the fleet (scripts/ci/package-test-lane.sh suite Packages/macOS/CmuxNext …; both scratch refs add the #23935 base-red patch so the package builds):

  • Red: the failing test at f65a9ec, job 6171d5b254cad839d43ce7bf, ScreenshotPrivacyMaskTests: onlyAPersonsOwnRunIsUnmasked fails with 3 issues (a .user run carrying callerApp, callerAgentSession or callerTerminal came out unmasked). The other 4 tests pass.
  • Green: the fix at 65b122a (the 238d980 tree), job 83aa0d75a687e7081c19f2e8, ActionOriginTests,ScreenshotPrivacyMaskTests: 11/11 and 5/5 pass.

Observed / Not run

  • The head 91e026a adds only the fail-closed throw when a masked composite can't be painted. A fleet app compile passed on the head (job 7a50e9fb5e1e, 86 s). No test reaches that branch: the suites above ran on the 238d980 tree, not the head.
  • I didn't do a live capture in a running app.

teamleaderleo added a commit that referenced this pull request Oct 22, 2026
Two open PRs (#23972, #23983) rework cmux window screenshot in
AppWindowImage and WindowImageLayer. Leave both files as on base: the
overlay cut is an NSWindow helper painted as an ordinary window layer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@teamleaderleo
teamleaderleo merged commit c4a8483 into feat-cmux-next Oct 22, 2026
@teamleaderleo
teamleaderleo deleted the cmux/window-screenshot-agents branch October 22, 2026 05:57
austinywang added a commit that referenced this pull request Oct 24, 2026
Conflicts resolved toward the base where it now owns the behavior:
- Agent screenshots: the base's policy (#23983, browser pages blacked out,
  captures off main) replaces this branch's pane/tab screenshot changes.
- Opening a session's chat in a pane: the base's agent.openSessionTab
  (#23492) replaces this branch's pane/workspace arguments on
  agent.openSessionWorkspace; agents chat new --pane|--workspace opens its
  tab through agent.openSessionTab, which now answers tab, pane, workspace
  and workspace_key.
- cmux docs: both sets of guides kept; the chat guide drops the keywords
  the base's setup guides use (prompt, harness).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo added a commit that referenced this pull request Oct 25, 2026
…nshotPrivacyMask

#23983 landed the same feature (agents screenshot the app, browser pages
blacked out) five hours after this PR opened, so base now has a second, weaker
mask. Resolved by keeping this PR's ScreenshotMask, which does everything
ScreenshotPrivacyMask does and more, and deleting the duplicate:

- the mask comes from the window's view tree, so a page a pane holds while its
  next tab paints, a sleeping page's picture and a page under no chrome view are
  covered; chats, Home views and every app page not in `shownAppPages` too;
- it samples each run loop turn across the capture, retries an unsettled window
  and refuses rather than return pixels it could not mask;
- `viaControlSocket` marks every socket run, so a caller claiming `origin: "user"`
  no longer gets the raw image (base's `applies(to:)` let it through).

Kept from #23983, which this PR's `isPersonsRun` did not cover: a run that names
a caller (`callerApp`, `callerAgentSession`, `callerTerminal`) is masked too,
even carrying `user` from the person's gesture inside it.

Takes back #23983's refusal of pane/tab screenshots over the socket when the
pane or tab shows a browser: masking the page area whole keeps that guarantee
without the refusal, which is this PR's point. `handlers.screenshot.browserNeedsREPL`
went with it.

Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a174ff-00c4-7cf2-b0d3-58b1149a6809&host=huge-blue
teamleaderleo added a commit that referenced this pull request Oct 25, 2026
… what an agent's capture shows now

Review (claude subagent, resolution delta) found three things in the hand
resolution:

- The package split moved a `!isHidden` guard from the chat launch image onto
  whatever `Rules.area` returned, which includes a browser chrome's page area.
  Before the split the page area was added unconditionally. That is fail-open in
  exactly the race the type exists for: hide the page area, capture inside the
  stale frame, and its pixels ship unmasked. Inert today (nothing assigns
  `contentContainer.isHidden`), fixed anyway: the rule decides, and only the
  launch image is gated.
- skills/cmux/SKILL.md and skills/cmux-browser/SKILL.md still described #23983's
  behaviour: "painted black" and "a pane showing a browser fails". Both are
  false here (gray, and masked rather than refused), and neither mentioned chats,
  Home views, app pages or the `masked` count. Rewritten to this head.
- Base's `theAppsOwnPageViewIsNotMasked` had no replacement, and it covers the
  branch the split made indirect. `aListedAppPageAndItsWebViewAreNotCovered`
  takes it: a listed page is not covered, an unlisted one is, and a listed page's
  own web view is not masked for being a web view.

Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a174ff-00c4-7cf2-b0d3-58b1149a6809&host=huge-blue
teamleaderleo added a commit that referenced this pull request Oct 25, 2026
Both independent reviews (Codex gpt-6-astra xhigh, and a Claude subagent on the
resolution delta) called this blocking, and they are right. #23983 masked the
whole BrowserChromeView; an earlier head of this PR masked only
`chrome.pageArea`, on the reasoning that an agent can read the URL through the
browser REPL anyway. That reasoning was never verified, and the address bar
carries a URL that can hold a token or a one-time link, plus the page's title
and badges. Weakening a landed gate needs a better argument than utility.

`Rules.app.area` returns the chrome itself, not its page area. It stays an
`area` rather than a `covers` so `read` still descends into the chrome and
still marks the mask unsettled when a page's pixels have moved out from under
it (aStrayWebViewIsMaskedAndAMovingPageIsUnsettled covers that). The two tests
that named the page rect now name the chrome rect.

Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a174ff-00c4-7cf2-b0d3-58b1149a6809&host=huge-blue
teamleaderleo added a commit that referenced this pull request Oct 26, 2026
…glass, and waits behind a busy main actor (#24240)

* test: a window snapshot waits behind a busy main actor; a background toggle reaches the Home (red)

debug.window_snapshot is now built by DebugWindowSnapshot.controlMethod (no
behavior change) so a test can register it. Both tests fail on this commit:
the snapshot times out at the control-plane deadline while the main actor is
busy, and with no key window a child window in front takes the Chief Settings
toggle instead of its window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: a window snapshot waits behind a busy main actor; a background toggle reaches the Home

debug.window_snapshot keeps its own 30 s deadline instead of the 2 s
control-plane one. On a capture mini that also runs builds and tests
(cua-5 at load 30 to 44 on 14 cores) the wait for the main actor passed
2 s, the snapshot timed out, and pr-capture refused the capture for want of
a still. The capture itself takes about 50 ms.

With no key window, home.toggleChiefSettings resolves the frontmost visible
window to the window it belongs to. Outside an active app AppKit lists
Chromium page child window first, so a toggle from the CLI with cmux in the
background (every capture slot) reached no Home and the popover never opened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Composite a window with its popovers and menus in one image

A window snapshot composited the window, then its overlay panels alone, then
the menus over it. A glass popover composited alone blurs nothing and came
out grey. The window, its child windows and the app's windows over it now go
to the window server as one composite, so the glass blurs the window under
it as on screen. Where a Chromium page image is painted over that, the
overlays come back on top through a coverage mask. Only this app's windows
are composited, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: a window screenshot awaits its fallback outside ??

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the capture tool change to debug.window_snapshot

Two open PRs (#23972, #23983) rework cmux window screenshot in
AppWindowImage and WindowImageLayer. Leave both files as on base: the
overlay cut is an NSWindow helper painted as an ordinary window layer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* debug.window_snapshot: a translucent overlay is not faded twice; cut overlays before the page awaits

Where the overlays alone are translucent (a shadow, a fade, an edge), the cut
now takes their own pixels instead of the composite times their alpha, which
painted over a page image faded them twice. The overlays are cut right after
the composite, before the page images' awaits, so a popover that opens
meanwhile is not cut from a composite it is not in. The deadline test waits
for the request to start before it blocks the main actor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a1603a-6462-72f0-a4b0-6937eb58e4c3&host=cosmic-gray

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo added a commit that referenced this pull request Oct 30, 2026
…es come back masked (#23972)

* cmux NEXT: agents can screenshot windows, panes and tabs; browser pages come back masked

Socket runs of window.screenshot, pane.screenshot and tab.screenshot are no
longer person-only. Any run the control socket carries (whatever origin it
claims) gets the window without child or floating windows, with every
browser page area, agent chat and conversation filled with an opaque mask
read from the view tree on every run loop turn around the capture. A window
that keeps animating or resizing, or a mask that cannot be painted, refuses
the capture. A browser tab no window shows stays refused to automation.
Design mode and the raw page captures stay person-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16047-2854-7333-9aff-632422c609d7&host=cmux16

* screenshots: mask every app page not checked to be safe, Home and debug.toast runs

Review findings on the masked socket capture:
- App pages are masked unless listed in ScreenshotMask.shownAppPages (keys,
  changelog, icon picker, apps, diff, markdown, editor, files, debug
  settings), so Settings, Passwords, History, Home Channels, Agent Activity,
  the Chief pages and any new page are masked by default.
- The Home list, dock, draft and transcript views are masked too (the list
  shows each conversation's latest message).
- debug.toast's runs are socket runs: a screenshot it starts stays masked.
- The handler's raw-or-masked choice is tested through
  ScreenshotHandlers.shown with real invocations, pane crop included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16047-2854-7333-9aff-632422c609d7&host=cmux16

* screenshots: keep the settle timer alive with a synchronous lifetime fence

withExtendedLifetime with an async body does not compile on CI's SDK. The
header now names everything a socket capture masks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16047-2854-7333-9aff-632422c609d7&host=cmux16

* test(screenshots): no nested #require; no shadowed locals in the pixel reader

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16047-2854-7333-9aff-632422c609d7&host=cmux16

* screenshots: the mask engine moves to CmuxNextScreenshots; the app supplies the view rules

The app-size guardrail refuses new logic in CmuxNextApp, and refuses appending
it to an existing App file too. ScreenshotMask could not move whole: it names
BrowserChromeView, AgentPaneView, the Home views, PageWebView and
PaneContentView, which the App owns until the Workbench rows of the refactor
plan move.

So the geometry and the timing (the regions, the settle wait, the attempts, the
run-loop Sampler, whether a view's pixels are where its frame says) go to
CmuxNextScreenshots beside WindowImageLayer, and the view types stay in the App
as ScreenshotMask.Rules.app in AppWindowImage.swift: covers, area,
isAppPage, coversContents. CmuxNextScreenshots gains one dependency,
CmuxNextWakeups, for the DemandTimer the settle wait uses.

Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a174ff-00c4-7cf2-b0d3-58b1149a6809&host=huge-blue

* screenshots: import CmuxNextScreenshots where ScreenshotMask is named

The fleet compile at 018aac1 found the two files the move left without it:
ScreenshotHandlers' `catch let failure as ScreenshotMask.Failure` and
ScreenshotStrings' `reason(_ failure: ScreenshotMask.Failure)`.

Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a174ff-00c4-7cf2-b0d3-58b1149a6809&host=huge-blue

* screenshots: the page area is masked even when hidden; the skills say what an agent's capture shows now

Review (claude subagent, resolution delta) found three things in the hand
resolution:

- The package split moved a `!isHidden` guard from the chat launch image onto
  whatever `Rules.area` returned, which includes a browser chrome's page area.
  Before the split the page area was added unconditionally. That is fail-open in
  exactly the race the type exists for: hide the page area, capture inside the
  stale frame, and its pixels ship unmasked. Inert today (nothing assigns
  `contentContainer.isHidden`), fixed anyway: the rule decides, and only the
  launch image is gated.
- skills/cmux/SKILL.md and skills/cmux-browser/SKILL.md still described #23983's
  behaviour: "painted black" and "a pane showing a browser fails". Both are
  false here (gray, and masked rather than refused), and neither mentioned chats,
  Home views, app pages or the `masked` count. Rewritten to this head.
- Base's `theAppsOwnPageViewIsNotMasked` had no replacement, and it covers the
  branch the split made indirect. `aListedAppPageAndItsWebViewAreNotCovered`
  takes it: a listed page is not covered, an unlisted one is, and a listed page's
  own web view is not masked for being a web view.

Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a174ff-00c4-7cf2-b0d3-58b1149a6809&host=huge-blue

* screenshots: a browser pane is masked whole again, its toolbar included

Both independent reviews (Codex gpt-6-astra xhigh, and a Claude subagent on the
resolution delta) called this blocking, and they are right. #23983 masked the
whole BrowserChromeView; an earlier head of this PR masked only
`chrome.pageArea`, on the reasoning that an agent can read the URL through the
browser REPL anyway. That reasoning was never verified, and the address bar
carries a URL that can hold a token or a one-time link, plus the page's title
and badges. Weakening a landed gate needs a better argument than utility.

`Rules.app.area` returns the chrome itself, not its page area. It stays an
`area` rather than a `covers` so `read` still descends into the chrome and
still marks the mask unsettled when a page's pixels have moved out from under
it (aStrayWebViewIsMaskedAndAMovingPageIsUnsettled covers that). The two tests
that named the page rect now name the chrome rect.

Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a174ff-00c4-7cf2-b0d3-58b1149a6809&host=huge-blue

* test(screenshots): the fixture builds its WebKitEngine the way the other app tests do

`WebKitEngine()` has no such initializer: it takes `fileInput` and `stores`.
The CI test-target build at 77e0ee3 (`cmux-next generated files`) found it;
compile-check --app links no test target, so neither fleet compile could.
`WebKitEngine(fileInput: NoFileInput(), stores: .inMemory())` is what
NewTabHoverPreviewTests and AgentPageStaysInWindowTests already use.

Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a174ff-00c4-7cf2-b0d3-58b1149a6809&host=huge-blue

* cmux NEXT screenshots: say why a hidden view adds no mask area

The comments said a hidden browser chrome stays masked, which read() never
did. The real guarantee is the sampler's: its first reading comes settle
(100 ms) before the capture, so a view hidden before it is off the screen,
and one hidden after it was in an earlier reading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3

* ci: list MaskedScreenshotTests among the display suites

Its two pixel tests carry a display skip trait; display_suites.py --check
requires the suite in display-suites.txt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3

* fix(screenshots): mask browser tab strips

Agent-Session: https://cmux.com/deeplink/agent?id=aid-7247a8ee4eeb1a6e2a9e

* fix(screenshots): retain launch masks alongside tab strips

Agent-Session: https://cmux.com/deeplink/agent?id=aid-7247a8ee4eeb1a6e2a9e

* test(screenshots): verify saved frames remain masked in both crops

Agent-Session: https://cmux.com/deeplink/agent?id=aid-7247a8ee4eeb1a6e2a9e

* fix(screenshots): mask unchecked native page tabs; refuse a capture whose areas move

An independent review at 7d7ec32 blocked on two fail-open paths:
- a native page tab (InternalPageView without a PageWebView: Phone and Mac
  Screen frames, Passwords, Stashed, a new page) was not masked, against
  the rule that an app page is masked until someone checks it. It is now
  covered whole unless its page id is in shownNativePages.
- the sampler kept every reading's areas but never noticed an area that
  moved between readings, whose pixels can sit between two frames. Such a
  move now makes the mask unsettled, so the capture retries or refuses.

Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(defaults): record this PR's screenshot defaults in the snapshot

The generated-files check found schemas/defaults/cmux-next.json stale at
5d6d95a. Only this PR's 24 keys (the screenshot mask, handlers and
strings, DebugToast.run) are updated, from the shared build's
regenerated file; keys that are equally stale on the base are left for
the base's own fix.

Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch was previously deployed

4 inactive (2 outdated) deployments
ci-routing-agent — 91e026ac Deployed Oct 22, 2026 by github-actions[bot]
cmux-tui-merge-tree — 91e026ac Deployed Oct 22, 2026 by teamleaderleo
sentry-dsyms — 238d9809 Deployed Oct 22, 2026 by teamleaderleo
artifacts — 238d9809 Deployed Oct 22, 2026 by teamleaderleo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant