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:
win32gui.EnumWindows() discovers valid top-level HWNDs on Windows.
- Cap'''s
WindowImpl::list() uses EnumChildWindows from the desktop window, which enumerates children of the desktop rather than top-level application windows.
- 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.
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
Exact observed problem
On Windows, the Cap CLI can be invoked successfully, but
cap targets windows --jsonreturns 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 tocap record start --window <id>through the normal CLI workflow.The CLI entrypoint is
apps/cli/src/targets.rs. Itswindows()function callscap_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 aWindowId, resolves the target withWindow::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 invokesEnumChildWindows(Some(GetDesktopWindow()), ...). This is the suspicious failure boundary:EnumChildWindowsenumerates child windows beneath a parent, whereas the application windows required bycap targets windowsare top-level windows and are discoverable throughEnumWindows.The same
win.rsfile already usesEnumWindowsinget_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()useswin32gui.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.pypasses 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.pywin32is 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.pyis 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.rswraps the nativeHWNDinWindowImpland converts the handle value toWindowIdImplwith the underlying numeric value cast tou64. Therefore, a live numeric HWND returned bywin32gui.EnumWindows()is conceptually the same native identifier that Cap uses for a WindowsWindowId, provided the window is still valid when recording begins.My current understanding is therefore:
win32gui.EnumWindows()discovers valid top-level HWNDs on Windows.WindowImpl::list()usesEnumChildWindowsfrom the desktop window, which enumerates children of the desktop rather than top-level application windows.WindowImpl::list()to useEnumWindowsdirectly, matching the pattern already present inget_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.rsthroughcrates/recording/src/sources/screen_capture/mod.rsintocrates/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.rsconfigures Direct3D capture and appliescrop_bounds, while the shared crop calculation is incrates/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 iscrates/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
--windowshould 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:
crates/scap-targets/src/platform/win.rsso visible top-level application windows can be discovered by the CLI.WindowIdand survives serialization throughapps/cli/src/targets.rs.cap targets windows --jsonwith a usable ID.cap record start --windowwithout claiming that target discovery alone resolves all window-cropping behavior.Acceptance criteria
cap targets windows --jsonreturns at least one target instead of[].crates/scap-targets/src/platform/win.rs.cap record start --window <id>without requiringpywin32in the consuming application.run.pyremains valid for consumers while the new PR is developed.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.