openai / openai/codex

[macOS] Computer Use get_app_state screenshot is clipped to the display; window portions beyond the display edge come back white (blank captures with AeroSpace/yabai and any half-offscreen window)

Open
#44,231 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug CLI computer-use
Dominant language
Rust
Stars
125k
Forks
19.4k
PR merge metrics
PR metrics pending

Description

Summary

SkyComputerUseService (com.openai.sky.CUAService 26.831.1000926, shipped with ChatGPT 26.901.51231 and used by Codex CLI 0.153.4 through the unified-computer-use plugin) captures app screenshots with a display-bound ScreenCaptureKit filter. Anything outside the display rectangle is rendered as opaque white. Tiling window managers such as AeroSpace hide inactive-workspace windows by parking them at a screen corner with one pixel left on the display, so every such window returns a solid white screenshot even though macOS can still capture it in full. The accessibility tree still works, so the agent acts blind.

Switching the single-window capture to SCContentFilter(desktopIndependentWindow:) (already linked in the service binary) returns the full window for every app tested except Final Cut Pro, which itself stops drawing while offscreen.

Environment

Component Version
macOS 26.5 (25F71), Apple M1 Max, two displays: 3440×1440 main at (0,0), 1920×1080 at (−1920,0)
ChatGPT desktop 26.901.51231 build 8109
Codex CLI 0.153.4, plugin unified-computer-use@openai-bundled 26.901.51231
Computer Use service ~/.codex/computer-use/Codex Computer Use.app, bundle com.openai.sky.CUAService, 26.831.1000926
Window manager AeroSpace 0.21.3-Beta

Reproduction (no window manager needed)

  1. Open TextEdit with any document. Float the window and place it so half of it hangs past the right edge of the display, e.g. AppleScript set bounds of window 1 to {2940, 200, 3940, 900} on a 3440-wide display.
  2. From Codex CLI: const app = await cua.getApp("com.apple.TextEdit"), then await app.getAXStateAndScreenshot().
  3. Result: a 1000×700 JPEG whose left half shows the document and whose right half is pure white (every pixel 255).
  4. Control: screencapture -l <windowID> or SCScreenshotManager.captureImage with SCContentFilter(desktopIndependentWindow:) on the same window ID returns the full window including the off-display half.

Measurements from the affected machine (September 9, 2026)

AeroSpace parks inactive windows at the bottom-right corner minus one pixel. CoreGraphics reports them as on-screen:

Window Bounds (x, y, w, h) Pixels inside a display
Final Cut Pro 3439, 1402, 1720, 1050 1 × 38
Mimestream 3439, 1388, 1147, 1409 1 × 52
Rambox (Electron) −2559, 1048, 640, 1049 1 × 32

Captures of the same window IDs, without moving anything:

Window Computer Use getScreenshot SCK display filter + sourceRect (my probe) SCK desktopIndependentWindow (my probe) screencapture -l
Mimestream, parked blank (recorded Sept 8) black, no content full UI, both halves full UI
Rambox, parked blank (recorded Sept 8) not run full UI, both halves full UI
TextEdit, half off-display left half UI, right half white left half UI, right half black full window, both halves full window
Final Cut Pro, parked blank black white black
Final Cut Pro, visible full UI (recorded) full UI full UI full UI

Occlusion check: with a floating TextEdit window placed on top of a visible Superset window, the Computer Use screenshot of Superset shows Superset content under the overlap, not TextEdit. So the current filter already excludes other windows; it only clips to the display rectangle.

What the binary shows

strings on SkyComputerUseService contains initWithDisplay:includingWindows:, initWithDesktopIndependentWindow:, captureImageWithFilter:configuration:completionHandler:, setSourceRect:, setShouldBeOpaque:, cannotClickOffscreenElement, and Capture Accelerated Window Screenshot. The observed clipping matches the display-including-windows filter with a source rect equal to the window frame.

Requested fix

Use SCContentFilter(desktopIndependentWindow:) for the per-window screenshot in get_app_state, with shouldBeOpaque and ignoreShadowsSingleWindow as today. Keep the display-bound path only as a fallback when the window is not in shareable content. This restores captures for windows parked by AeroSpace, yabai, and Amethyst, and for any window the user has dragged partly off-screen.

Known limit that the fix cannot cover: Final Cut Pro stops rendering when its window is almost entirely offscreen (all four capture methods return blank while it is parked and full content once visible), so it will still need to be on a display.

Reproduction probe

Swift probe used for the comparison rows above (needs Screen Recording permission; swiftc -O -o sckprobe sckprobe.swift, then ./sckprobe <windowID> out.png independent|display):

sckprobe.swift
import Foundation
import ScreenCaptureKit
import CoreGraphics
import ImageIO
import UniformTypeIdentifiers

// Usage: sckprobe <windowID> <out.png> [display|independent]
let args = CommandLine.arguments
let wid = CGWindowID(UInt32(args[1])!)
let out = URL(fileURLWithPath: args[2])
let mode = args.count > 3 ? args[3] : "independent"

func save(_ img: CGImage, _ url: URL) {
    let dest = CGImageDestinationCreateWithURL(url as CFURL, UTType.png.identifier as CFString, 1, nil)!
    CGImageDestinationAddImage(dest, img, nil); CGImageDestinationFinalize(dest)
}

_ = CGMainDisplayID() // init CGS connection
let sem = DispatchSemaphore(value: 0)
Task {
    do {
        let content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: false)
        guard let win = content.windows.first(where: { $0.windowID == wid }) else {
            print("window \(wid) not in shareable content (count=\(content.windows.count))"); sem.signal(); return }
        print("window:", win.windowID, win.owningApplication?.applicationName ?? "?", "frame:", win.frame, "isOnScreen:", win.isOnScreen, "isActive:", win.isActive)
        let filter: SCContentFilter
        if mode == "display" {
            let disp = content.displays.first(where: { $0.frame.intersects(win.frame) }) ?? content.displays[0]
            filter = SCContentFilter(display: disp, including: [win])
            print("mode: display-bound, display frame:", disp.frame)
        } else {
            filter = SCContentFilter(desktopIndependentWindow: win)
            print("mode: desktopIndependentWindow, window frame:", win.frame)
        }
        let cfg = SCStreamConfiguration()
        if mode == "display" {
            cfg.sourceRect = CGRect(x: win.frame.minX - filter.contentRect.minX, y: win.frame.minY - filter.contentRect.minY, width: win.frame.width, height: win.frame.height)
            cfg.width = Int(win.frame.width); cfg.height = Int(win.frame.height)
        } else {
            cfg.width = Int(win.frame.width) * 2; cfg.height = Int(win.frame.height) * 2
        }
        cfg.showsCursor = false; cfg.ignoreShadowsSingleWindow = true; cfg.shouldBeOpaque = true
        let img = try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: cfg)
        print("captured:", img.width, "x", img.height)
        save(img, out)
    } catch { print("ERROR:", error) }
    sem.signal()
}
sem.wait()

Happy to run further comparisons on this machine if useful. Cross-reference: #38348 is another ScreenCaptureKit-side capture issue in the same service.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the get_app_state screenshot path in SkyComputerUseService and compare it with the provided sckprobe.swift modes. Verify the desktopIndependentWindow capture on a partially off-screen window, then confirm completion with full captures for parked and half-offscreen windows while preserving the documented Final Cut Pro limitation.

Written by the indexing model from the issue text.

Assessment

Tech stack
macos, swift
Domain
desktop
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.