Skip to content

Replace legacy window screenshot capture - #9066

Merged
austinywang merged 32 commits into
mainfrom
issue-9065-remove-cgwindowlistcreateimage
Aug 7, 2026
Merged

austinywang merged 32 commits into
mainfrom
issue-9065-remove-cgwindowlistcreateimage

Conversation

@austinywang

@austinywang austinywang commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Remove the deprecated CGWindowListCreateImage window-capture API so a future SDK removal cannot prevent cmux from loading.
  • Keep the v1 screenshot and v2 debug.window.screenshot commands on one app-owned socket-worker path, with exact window-ID conversion and filename-component sanitization.
  • Prefer the permission-free current-process ScreenCaptureKit path on macOS 14.4+; on macOS 14.0–14.3 use ScreenCaptureKit only when access is already granted, then fall back to AppKit without prompting.
  • Bound ScreenCaptureKit and AppKit independently until their asynchronous work actually retires, so a stalled backend cannot accumulate duplicate work or block the permission-free fallback.
  • Make the AppKit fallback capture native window chrome and composite Ghostty IOSurface pixels, WKWebView snapshots, native occluders, and only explicitly cmux-owned WebKit overlays. WebKit internal native views are never mistaken for cmux UI.
  • Add behavior coverage for routing, capture admission, permission policy, window IDs, label safety, clipping/native overlays, and a socket-driven PNG test containing both terminal and browser pixels.

Fixes #9065

Testing

Demo Video

N/A — this is a DEBUG socket screenshot path; the focused E2E validates generated PNG pixels.

Review Trigger (for human review)

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

Checklist

  • I ran the relevant test suite(s) locally or in CI.
  • I added or updated tests for behavior changes.
  • I did not add unrelated changes.
  • I did not commit secrets, credentials, build artifacts, or generated caches.
  • I did not add product docs unless explicitly requested.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Updates window screenshot capture to use the AppKit PNG path exclusively and adds a UI regression test that verifies the returned PNG contains non-blank terminal content.

Changes

Window screenshot validation

Layer / File(s) Summary
AppKit screenshot capture
Sources/TerminalController.swift, Sources/GhosttyTerminalView.swift
Uses only AppKit window PNG capture, removes the composited capture helper, and clarifies related permission documentation.
Screenshot command test
cmuxUITests/AutomationSocketUITests.swift
Adds socket-driven coverage that creates terminal content, invokes debug.window.screenshot, polls for a marker, and validates the PNG response.
PNG pixel analysis
cmuxUITests/AutomationSocketUITests.swift
Decodes PNG data and checks dimensions, color diversity, and marker-color pixels.

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

Possibly related issues

  • #6785 — Directly concerns the screenshot regression test added in this change.
  • #6775 — Concerns the same screenshot test change.
  • #6786 — Reports the newly added screenshot test in AutomationSocketUITests.swift.

Suggested reviewers: lawrencecchen

🚥 Pre-merge checks | ✅ 24 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (24 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The legacy CGWindowList path is removed, AppKit remains the primary screenshot path, and regression coverage was added.
Out of Scope Changes check ✅ Passed The changes stay focused on screenshot capture, its test coverage, and a minor documentation comment update.
Cmux Swift Actor Isolation ✅ Passed Only an XCTest changed in the actual commit; no production Swift actor-isolation regressions were introduced.
Cmux Swift Blocking Runtime ✅ Passed The diff only loosens an XCTest expected-failure assertion in a test file; it adds no new blocking, sleeps, polling, or sync primitives in production Swift.
Cmux Browser Automation Off-Main ✅ Passed PR only changes window screenshot capture/tests; no browser.* command routing changed. Existing browser.screenshot remains worker-routed and policy-tested.
Cmux Expensive Synchronous Load ✅ Passed Only changed file is a UI test; no production Swift, agent-history loaders, or main-actor interactive load paths were added.
Cmux Cache Substitution Correctness ✅ Passed The diff only loosens a test activation matcher; no production snapshot/cache substitution was added or changed.
Cmux No Hacky Sleeps ✅ Passed Only a Swift UI test changed; no non-Swift app/runtime scripts with hacky sleeps or waits were added.
Cmux Algorithmic Complexity ✅ Passed The actual PR diff only relaxes a UI test activation matcher; no production code or scalable collection logic changed.
Cmux Swift Concurrency ✅ Passed Diff only widens an XCTest assertion in test code; it adds no legacy async patterns or other concurrency regressions.
Cmux Swift @Concurrent ✅ Passed Touched Swift changes are synchronous; no new @concurrent/nonisolated-async annotations appear, and screenshot capture stays behind v2MainSync/captureAppKitWindowPNGData.
Cmux Swift Package Boundaries ✅ Passed Only app-specific AppKit glue and UITest-only pixel analysis were added; no reusable domain logic or shared feature code was kept in the app target.
Cmux Swiftpm Lockfiles ✅ Passed Diff only touches source and UI tests; no .gitignore, Package.swift, Package.resolved, or Xcode package-reference files changed, so the lockfile rule isn’t implicated.
Cmux Swift Logging ✅ Passed The only diff is a test assertion tweak; no new or changed print/debugPrint/dump/NSLog/logging code appears in the patch.
Cmux User-Facing Error Privacy ✅ Passed Only a test assertion was broadened; no production user-facing errors, alerts, or command output were added or changed.
Cmux Full Internationalization ✅ Passed Only test code and a developer-facing doc comment changed; no new user-facing localized text or catalog entries were added.
Cmux Swiftui State Layout ✅ Passed No new SwiftUI state/layout anti-patterns were introduced; the diff only changes screenshot capture/test code and a doc comment, with legacy AppKit bridge state unchanged.
Cmux Architecture Rethink ✅ Passed Production code now has one clear screenshot owner and removes CGWindowList; the only polling is confined to a new UI test, which the rules allow.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No standalone cmux-owned windows were added or changed; the PR only updates screenshot capture and a test fixture, so the auxiliary-window close-shortcut rule doesn’t apply.
Cmux Source Artifacts ✅ Passed PASS: The only changed path is cmuxUITests/AutomationSocketUITests.swift, a hand-written test source file; no artifact paths (tmp, screenshots, logs, caches, DerivedData) are added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Production Sources only remove the legacy capture helper and tweak an existing debug doc comment; no new test/debug seam was added.
Cmux No Ambient Global State ✅ Passed PASS: Production diffs only remove the legacy screenshot helper and tweak a comment; no new file-scope funcs, globals, or singletons were added.
Title check ✅ Passed The title clearly and concisely describes the primary change: replacing legacy window screenshot capture.
Description check ✅ Passed The description includes the required summary, testing, demo video note, review trigger, and checklist information.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-9065-remove-cgwindowlistcreateimage

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cmuxUITests/AutomationSocketUITests.swift (1)

253-256: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Replace the fixed sleep 60 with a non-time-based blocker.

This test fixture uses an arbitrary wall-clock delay only to keep the shell alive. Use a blocking command such as tail -f /dev/null, allowing teardown to end it without coupling test correctness to a 60-second timeout.

Proposed fix
-        i=0; while [ $i -lt 8 ]; do printf '\033[48;2;245;40;210m%-80s\033[0m\n' '\(marker)'; i=$((i+1)); done; sleep 60
+        i=0; while [ $i -lt 8 ]; do printf '\033[48;2;245;40;210m%-80s\033[0m\n' '\(marker)'; i=$((i+1)); done; tail -f /dev/null
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxUITests/AutomationSocketUITests.swift` around lines 253 - 256, Replace
the fixed sleep in the markerCommand fixture with a non-time-based blocking
command such as tail -f /dev/null, keeping the shell alive until teardown
terminates it. Preserve the existing marker output loop and command behavior
before the blocker.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxUITests/AutomationSocketUITests.swift`:
- Around line 239-243: In the app launch setup around app.launch(), narrow the
handling to the known headless foreground-activation race rather than marking
all launch failures as expected. Remove the broad XCTExpectFailure wrapper and
use targeted conditional handling with authoritative diagnostics, skipping only
when that specific headless behavior is detected while preserving genuine launch
failures.

---

Outside diff comments:
In `@cmuxUITests/AutomationSocketUITests.swift`:
- Around line 253-256: Replace the fixed sleep in the markerCommand fixture with
a non-time-based blocking command such as tail -f /dev/null, keeping the shell
alive until teardown terminates it. Preserve the existing marker output loop and
command behavior before the blocker.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 23cc9170-e37c-423a-bb9a-1277b7b81d3b

📥 Commits

Reviewing files that changed from the base of the PR and between b9b51b2 and 26f31b7.

📒 Files selected for processing (1)
  • cmuxUITests/AutomationSocketUITests.swift

Comment thread cmuxUITests/AutomationSocketUITests.swift
@cursor

cursor Bot commented Jul 28, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@austinywang

Copy link
Copy Markdown
Contributor Author

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@cubic-dev-ai

cubic-dev-ai Bot commented Jul 28, 2026

Copy link
Copy Markdown

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@austinywang I can't start this review because your workspace has reached its free monthly review limit. cubic has reviewed 241,631 of the 240,000 allowed lines of code this month. Reviews resume on 1 August 2026 (in 4 days). Paid plans include much higher monthly review limits. Upgrade now to resume reviews.

To help optimise your usage, you can tune cubic to get the most out of your usage limits:

Learn more →

@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown

@austinywang: I’ll review the changes in #9066.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
cmuxUITests/AutomationSocketUITests.swift (2)

284-287: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Wait for rendered content, not only terminal-buffer content.

surface.read_text proves that the marker reached the terminal model, but not that AppKit has rendered it before debug.window.screenshot runs. Poll the screenshot result until markerPixels is above the threshold, or use an authoritative render-completion signal, to prevent CI flakes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxUITests/AutomationSocketUITests.swift` around lines 284 - 287, Update the
screenshot synchronization around waitForTerminalText so it waits for rendered
output rather than only terminal-buffer state. Poll debug.window.screenshot
until markerPixels exceeds the required threshold, or use an equivalent
authoritative render-completion signal, before capturing the final screenshot.

334-352: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Avoid direct wall-clock polling in the test helper.

Date() plus RunLoop.current.run introduces a real wall-clock dependency. Use an XCTest expectation/waiter around the socket predicate, or an injected virtual clock, while retaining a bounded timeout.

As per coding guidelines and path instructions, tests must avoid direct wall-clock APIs and use completion signals, virtual clocks, or approved bounded predicate polling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxUITests/AutomationSocketUITests.swift` around lines 334 - 352, Update
waitForTerminalText to remove direct Date() and RunLoop.current polling, and use
an XCTest expectation/waiter with a bounded timeout around the socket.read_text
predicate. Preserve the existing surfaceID lookup, expectedText matching, and
Bool result behavior.

Sources: Coding guidelines, Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@cmuxUITests/AutomationSocketUITests.swift`:
- Around line 284-287: Update the screenshot synchronization around
waitForTerminalText so it waits for rendered output rather than only
terminal-buffer state. Poll debug.window.screenshot until markerPixels exceeds
the required threshold, or use an equivalent authoritative render-completion
signal, before capturing the final screenshot.
- Around line 334-352: Update waitForTerminalText to remove direct Date() and
RunLoop.current polling, and use an XCTest expectation/waiter with a bounded
timeout around the socket.read_text predicate. Preserve the existing surfaceID
lookup, expectedText matching, and Bool result behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 692c7c10-c246-448f-a453-265d76044ceb

📥 Commits

Reviewing files that changed from the base of the PR and between ba63392 and e8d3600.

📒 Files selected for processing (1)
  • cmuxUITests/AutomationSocketUITests.swift

@austinywang

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@austinywang

Copy link
Copy Markdown
Contributor Author

@lawrencecchen The final implementation is stable at f1667eabb6: the tagged Debug build and focused terminal+browser screenshot E2E are green, all review threads are resolved, and full CI currently has 13 jobs green with its last 3 jobs running. When you have a moment, please give the final diff a human review and approve if it looks good. This PR will not be merged by this loop.

@cursor

cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 13 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/TerminalController.swift">

<violation number="1" location="Sources/TerminalController.swift:13308">
P3: When the AppKit capture times out (socketAwaitCallback returns nil after 5s), the caller returns "Failed to create PNG data", but `captureTask?.cancel()` does not stop the already-scheduled `Task { @MainActor in ... }`. `Task.cancel()` only flips a flag; the body here never checks `Task.isCancelled`, so the queued main-actor closure still runs: it performs the full `BrowserScreenshotWebViewSnapshotter.captureVisibleViewport` (up to 2s more), draws overlays into the now-discarded bitmap, and fires the detached `completion` (an extra `semaphore.signal()` into a waiter that already gave up). This is orphaned, side-effecting main-actor work and a stray completion racing with the next screenshot request. Consider cancel-and-returning only the synchronous value, or checking `Task.isCancelled` in the body and bailing before the WebKit snapshot/overlay work.</violation>

<violation number="2" location="Sources/TerminalController.swift:13352">
P2: Windows with three stalled browser panes fail screenshot capture after 5 seconds: each viewport snapshot may consume 2 seconds serially, exceeding the enclosing AppKit waiter. Use one shared deadline/budget (or otherwise bound/cancel the aggregate work) so the inner capture finishes before the outer timeout.</violation>

<violation number="3" location="Sources/TerminalController.swift:13426">
P2: Screenshots show externally composited browser/terminal content at full opacity during ancestor fade/hidden-by-alpha states. Track effective ancestor alpha and apply it when drawing the overlay (or skip an effectively transparent subtree).</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread Sources/TerminalController.swift Outdated
Comment thread Sources/TerminalController.swift Outdated
Comment thread Sources/TerminalController.swift Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 13 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Wire/ControlCommandExecutionPolicy.swift">

<violation number="1" location="Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Wire/ControlCommandExecutionPolicy.swift:148">
P2: Concurrent v2 screenshot requests can return a successful but incomplete AppKit PNG rather than the promised explicit busy error. The `.busy` path falls back after AppKit already reported missing external content, so WebKit-backed pixels can disappear; make a busy ScreenCaptureKit admission fail the request instead of selecting that fallback.</violation>
</file>

<file name="Sources/TerminalController.swift">

<violation number="1" location="Sources/TerminalController.swift:13193">
P2: The AppKit capture is gated first and is required to succeed; the ScreenCaptureKit path is only consulted afterwards and only when the AppKit capture returned a (partial) PNG. If the AppKit path times out — for example a WKWebView snapshot hangs or the window can no longer be located in `NSApp.windows` — `captureScreenshot` returns `ERROR: Failed to create PNG data` and never reaches SCK. Since on macOS 14.4+ SCK is the permission-free, compositor-based backend that doesn't depend on WebKit drawing, this ordering means a slow AppKit loss turns into a hard screenshot failure instead of falling back to the more robust backend. Consider attempting the SCK capture when the AppKit delivery times out (returning a `.unavailable`-style signal rather than nil on timeout) so SCK can still serve as the fallback.</violation>

<violation number="2" location="Sources/TerminalController.swift:13463">
P2: Overlapping native views are captured first and then overwritten by terminal/WebKit pixels, so screenshots can show terminal or browser content through UI that should occlude it. Composite these replacements at their hierarchy z-position, or use the compositor whenever an external layer overlaps other captured content.</violation>
</file>

<file name="cmuxUITests/AutomationSocketUITests.swift">

<violation number="1" location="cmuxUITests/AutomationSocketUITests.swift:686">
P3: The new responseTimeout parameter is honored by the primary ControlSocketClient path, but the netcat fallback (controlSocketJSONViaNetcat) that runs when the primary path fails still uses a fixed/short timeout. Callers in this PR pass 10–12s timeouts for browser.navigate/browser.wait/debug.window.screenshot because those commands legitimately take seconds; when the primary socket client happens to fail, the fallback ignores that longer timeout and can time out early, making an otherwise-valid capture appear to fail and flaking the new regression test. Consider routing the same timeout into the netcat fallback (or skipping the fallback for these long-running commands) so both paths honor the requested response timeout.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread Sources/TerminalController.swift Outdated
Comment thread Sources/TerminalController.swift Outdated
Comment thread cmuxUITests/AutomationSocketUITests.swift Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 13 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread Sources/TerminalController.swift
Comment thread Sources/TerminalController.swift Outdated
Comment thread Sources/TerminalController.swift Outdated
Comment thread cmuxUITests/AutomationSocketUITests.swift
@austinywang
austinywang merged commit ce41f15 into main Aug 7, 2026
55 of 61 checks passed
@austinywang
austinywang deleted the issue-9065-remove-cgwindowlistcreateimage branch August 7, 2026 05:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace CGWindowListCreateImage in window screenshot path — deprecated, slated for removal in a future macOS

1 participant