Skip to content

Fix Cap Windows window target discovery returning [] from cap targets windows --json #2164

Description

@matthewdonsemail-lab

Summary

I am documenting a reproducible Windows limitation in the Cap CLI and intend to implement the resolution in a new pull request against the Cap repository after validating the fix locally. My production consumer is the Robotic Video Sales Letter agent in run.py, where I need Cap to record the Camoufox browser window rather than the entire desktop.

Environment

  • Windows edition: Windows 11 Home Single Language
  • Version: 24H2
  • Installed: 4/6/2025
  • OS build: 26100.4652
  • Experience: Windows Feature Experience Pack 1000.26100.128.0

Exact observed problem

On Windows, the Cap CLI can be invoked successfully, but cap targets windows --json returns an empty JSON array ([]) even while a visible Camoufox top-level application window is open. Because no target is exposed, the result cannot be passed to cap record start --window <id> through the normal CLI workflow.

The CLI entrypoint is apps/cli/src/targets.rs. Its windows() function calls cap_recording::screen_capture::list_windows(), then serializes each returned window into the JSON target shape. An empty result at this boundary therefore becomes []; this is not a JSON formatting problem in the CLI.

The recording target abstraction is defined in crates/recording/src/sources/screen_capture/mod.rs. ScreenCaptureTarget::Window { id } expects a WindowId, resolves the target with Window::from_id(id), obtains the display and window bounds, and derives crop information from those bounds.

The Windows target implementation is crates/scap-targets/src/platform/win.rs. WindowImpl::list() defines an enumeration callback for windows, but currently invokes EnumChildWindows(Some(GetDesktopWindow()), ...). This is the suspicious failure boundary: EnumChildWindows enumerates child windows beneath a parent, whereas the application windows required by cap targets windows are top-level windows and are discoverable through EnumWindows.

The same win.rs file already uses EnumWindows in get_topmost_at_cursor_fallback(). This makes the list path internally inconsistent: the general window-list path begins with desktop-child enumeration, while another Windows path enumerates top-level windows directly.

Evidence from my repository

My consumer-side implementation is run.py. _find_camoufox_hwnd() uses win32gui.EnumWindows() to enumerate visible top-level windows, filters titles and process information, and returns the Camoufox window handle when it finds a match. This independently finds a valid HWND while Cap target discovery returns [].

The same run.py passes the discovered value to the Cap recording command when a window ID is available and retains a screen-recording fallback when it is not. The workaround is intentionally explicit because Cap target discovery is currently unavailable on my Windows setup.

The workaround is documented in README.md, which records that the Cap Windows target command returns an empty list and that pywin32 top-level enumeration is used as a consumer-side fallback. pywin32 is not doing the recording; it is only discovering an HWND and allowing my runner to identify the browser window.

The browser and cursor implementation in camoufox_capability.py is separate from this failure. It drives Camoufox and harvests browser events; it does not provide Cap target enumeration and is not the source of the empty Windows target array.

HWND and Cap identifier relationship

The Windows implementation in crates/scap-targets/src/platform/win.rs wraps the native HWND in WindowImpl and converts the handle value to WindowIdImpl with the underlying numeric value cast to u64. Therefore, a live numeric HWND returned by win32gui.EnumWindows() is conceptually the same native identifier that Cap uses for a Windows WindowId, provided the window is still valid when recording begins.

My current understanding is therefore:

  1. win32gui.EnumWindows() discovers valid top-level HWNDs on Windows.
  2. Cap'''s WindowImpl::list() uses EnumChildWindows from the desktop window, which enumerates children of the desktop rather than top-level application windows.
  3. The fix should switch WindowImpl::list() to use EnumWindows directly, matching the pattern already present in get_topmost_at_cursor_fallback().

Scope of this issue

The primary scope is Windows target discovery for the Cap CLI, specifically the path from apps/cli/src/targets.rs through crates/recording/src/sources/screen_capture/mod.rs into crates/scap-targets/src/platform/win.rs.

This is distinct from a second possible limitation: a discovered window ID may still produce display-region cropping rather than true isolated window-content capture. The Windows capture implementation in crates/recording/src/sources/screen_capture/windows.rs configures Direct3D capture and applies crop_bounds, while the shared crop calculation is in crates/recording/src/sources/screen_capture/mod.rs. Fixing enumeration should restore target discoverability, but it must not be presented as automatically fixing capture semantics or crop correctness.

macOS caveat and platform-agnostic contract:
The macOS implementation is separate in crates/scap-targets/src/platform/macos.rs, where native Core Graphics window identifiers are used instead of Windows HWNDs. The macOS capture source is crates/recording/src/sources/screen_capture/macos.rs. The platform-specific discovery code must remain platform-appropriate.

However, the shared contract is the same across platforms: a listed window target must resolve to the native window, its ID must be passed unchanged through the CLI and recording layers, and --window should represent the window content rather than merely a rectangle on a display. Existing macOS reports such as Cap issue #1754 indicate that target discovery and window-content capture are separate concerns. I will keep the Windows discovery fix focused while adding or proposing cross-platform regression coverage at the shared target boundary where appropriate.

Intended resolution in a new PR

I intend to fork or branch the Cap repository, reproduce the failure on the Windows environment listed above, and submit a new PR that:

  • Corrects the Windows window-list enumeration path in crates/scap-targets/src/platform/win.rs so visible top-level application windows can be discovered by the CLI.
  • Preserves and verifies the existing validity filters, including visibility, child-window exclusion, tool-window exclusion, ignored executables, and exclusion of Cap-owned windows where those filters are intentional.
  • Verifies that a returned native HWND becomes the expected WindowId and survives serialization through apps/cli/src/targets.rs.
  • Adds a regression test or the strongest available platform test proving that a visible top-level Windows test window appears in cap targets windows --json with a usable ID.
  • Verifies the ID handoff into cap record start --window without claiming that target discovery alone resolves all window-cropping behavior.
  • Keeps macOS and other platform implementations unchanged unless shared tests expose a genuine contract problem; platform-specific native enumeration should not be replaced with a Windows-specific assumption.

Acceptance criteria

  • On Windows, with a visible eligible application window open, cap targets windows --json returns at least one target instead of [].
  • The returned target contains a numeric ID that corresponds to the native Windows window handle representation used by crates/scap-targets/src/platform/win.rs.
  • The ID can be supplied to cap record start --window <id> without requiring pywin32 in the consuming application.
  • Invalid, hidden, child, tool, ignored, and Cap-owned windows remain filtered according to the intended behavior of the existing implementation.
  • The existing screen fallback in run.py remains valid for consumers while the new PR is developed.
  • Window-content isolation and crop behavior are tested or explicitly tracked separately from the empty-target discovery bug.

I am opening this issue before implementing the new PR so the failure boundary, consumer workaround, Windows-specific evidence, cross-platform caveat, and intended acceptance criteria are explicit.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions