Skip to content

feat(cua-driver-rs): Phase 2 panel + structural fixes (opt-in) - #1566

Merged
f-trycua merged 2 commits into
mainfrom
feat/cua-driver-rs-permissions-panel-phases-2-3
May 18, 2026
Merged

feat(cua-driver-rs): Phase 2 panel + structural fixes (opt-in)#1566
f-trycua merged 2 commits into
mainfrom
feat/cua-driver-rs-permissions-panel-phases-2-3

Conversation

@f-trycua

@f-trycua f-trycua commented May 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Builds on #1565 (Phase 1 panel). Lands Phase 2 of the native NSPanel onboarding gate AND a handful of structural macOS fixes that surfaced during live testing on this Mac. Key user-facing change: the panel is now opt-in via CUA_DRIVER_RS_PERMISSIONS_PANEL=1 rather than auto-shown when the daemon is launched from the bundled .app. The terminal banner is the documented default again.

Why opt-in

During live testing the panel works once you grant TCC manually — but a chain of macOS behaviours makes the "auto-shown" default fragile in dev environments:

  • Cargo linker-signing identifier: Cargo's default ad-hoc sign uses a per-build random identifier like cua_driver-9402b19cac15ebc5, not the com.trycua.driver bundle id. TCC keys registrations under the codesign identifier, so dev builds never appear in System Settings → Privacy & Security. Manual codesign --force --sign - -i com.trycua.driver after build fixes it; CD's Developer-ID signing step already does. (Follow-up: bake this into install-local.sh and/or a Makefile target.)
  • tccutil reset doesn't invalidate the session-level TCC trust cache: an in-process AXIsProcessTrusted() call still returns the cached "trusted" state after a reset, so the gate short-circuits before the panel can be presented. Only a logout/reboot fully clears the cache.
  • The panel relies on [NSApp runModalForWindow:] + a 1Hz NSTimer in NSModalPanelRunLoopMode (NOT NSRunLoopCommonModes, which silently drops timer fires in modal sessions); that needed a specific fix and is fragile to future changes.

Until those are stabilised end-to-end, terminal flow remains the default. The panel can be exercised by power users via CUA_DRIVER_RS_PERMISSIONS_PANEL=1.

What's in this PR

Phase 2 panel code (`crates/platform-macos/src/permissions/panel.rs`)

  • Live status rows: each row's NSImageView + title NSTextField are stashed as `RowHandles` in a thread-local. The poll callback swaps the SF Symbol (`checkmark.circle.fill` / `xmark.circle.fill`), tint (systemGreenColor / systemRedColor), and label text colour (`labelColor` → `secondaryLabelColor` on grant) in place.
  • Dynamic heading + subheading: `heading_for(status)` and `subheading_for(status)` produce verbatim parity with Swift's matrix ("CuaDriver needs your permission" → "One more permission" → "CuaDriver is ready"). Rebound via `setStringValue:` on every tick.
  • Auto-dismiss on all-green: the 1Hz `NSTimer` calls `[NSApp stopModal]` when both grants flip green, and `show_modal` returns `PanelOutcome::AllGranted` so the gate caller can skip the trailing `wait_for_grants` loop entirely.
  • Modal-mode timer fix: timer constructed with `timerWithTimeInterval:` (non-scheduled) and added to `NSModalPanelRunLoopMode` explicitly. The previous Phase 1 approach of adding to `NSRunLoopCommonModes` doesn't fire during `runModalForWindow:` — that bit me live.
  • Ready strip: green pill at the bottom (initially hidden, shown when both green) for the case where the panel is opened while already-granted.
  • Opt-in env var: `panel_enabled()` requires `CUA_DRIVER_RS_PERMISSIONS_PANEL` set to `1/true/yes/on` (case-insensitive). Default unset → terminal flow.

Gate restructuring (`crates/platform-macos/src/permissions/gate.rs`)

  • Hoist `request_accessibility()` / `request_screen_recording()` to before any UI: these calls have the critical side effect of registering the calling process with TCC. Without the hoist, when the user clicked "Open System Settings" mid-panel they landed in a Privacy & Security pane where cua-driver was missing from the list. Now the registration happens first, so the list is populated whichever path the user takes.
  • New `PanelPresentation::ShownAllGranted` variant: when the panel's poll resolves before the user clicks anything, the gate skips the trailing `wait_for_grants` loop entirely.

Structural macOS fixes

  • Bundle-skeleton rename: `libs/cua-driver-rs/scripts/CuaDriver.app/` → `scripts/CuaDriverBundle/`. The previous `.app` suffix on the repo template caused macOS LaunchServices on developer machines to index it as a second installed app with the `com.trycua.driver` bundle id, surfacing a "ghost CuaDriver" entry in System Settings → Privacy & Security pointing at an empty `Contents/MacOS/` directory. Dropping the `.app` suffix kills the indexing.
  • CD workflow + install-script comments + PARITY.md updated to point at the new path.
  • Docs (`docs/.../getting-started/installation.mdx`) rewritten to describe opt-in semantics.

Test plan

  • `cargo test -p platform-macos --lib permissions::` — 13/13 green (2 new parity tests for the heading-text matrix).
  • `cargo build --release -p cua-driver --bin cua-driver` — clean.
  • Live: panel opt-in (`CUA_DRIVER_RS_PERMISSIONS_PANEL=1`) renders with live rows updating as grants flip, auto-dismisses on all-green.
  • Live: default (no env var) takes the terminal flow with no regression.
  • Live: ghost-bundle entry gone from System Settings after the rename.
  • CD lane builds correctly with the renamed skeleton.
  • Post-merge full re-install via canonical install script verifies the end-to-end flow.

Related

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added optional experimental native permissions panel for macOS (opt-in via environment variable).
  • Improvements

    • Permissions onboarding now displays live-updating feedback as requirements are met.
    • Clarified accepted environment variable values for permissions control.
  • Documentation

    • Updated macOS installation guide with permissions flow behavior details.
    • Updated bundle packaging structure documentation.

Review Change Stack

f-trycua and others added 2 commits May 18, 2026 18:49
…y default

Adds Phase 2 of the native NSPanel onboarding gate (live status rows
with red/green icons, dynamic heading, auto-dismiss on all-green via a
1 Hz NSTimer in NSModalPanelRunLoopMode) and a handful of structural
fixes that surfaced during live testing.

Critical change in user-facing default: the panel is now **opt-in**.
The previous default of "panel when launched from /Applications/CuaDriver.app"
relied on a chain of macOS behaviours that break in practice:

  * Cargo's default linker-signing identifier (`cua_driver-<random>`)
    differs from the bundle id, so TCC keys registrations under the
    random hash and the bundle never appears in System Settings.
  * `tccutil reset` invalidates the database row but not the session-
    level TCC trust cache, so an in-process AXIsProcessTrusted() call
    still returns the cached "trusted" state — the gate short-
    circuits before the panel can be presented.
  * The cursor overlay's NSApp run-loop coexistence works in the
    Serve arm today but is fragile to future changes; an opt-in
    default reduces blast radius if the assumption regresses.

Until those are stabilised end-to-end, the terminal flow stays the
default (banner + 1 Hz poll + "still waiting on: X" status lines).
The panel still exists and can be exercised with
`CUA_DRIVER_RS_PERMISSIONS_PANEL=1`. Accepted on-sentinels (case-
insensitive): 1, true, yes, on.

Structural fixes bundled in this commit:

  * Rename `libs/cua-driver-rs/scripts/CuaDriver.app/` to
    `scripts/CuaDriverBundle/`. The previous `.app` suffix on the
    repo-template caused macOS LaunchServices on developer machines
    to index it as a second installed app with the `com.trycua.driver`
    bundle id, surfacing a "ghost CuaDriver" entry in System
    Settings → Privacy & Security pointing at an empty
    Contents/MacOS/ directory. Dropping the `.app` suffix removes
    the indexing. CD workflow + install-script comments updated to
    point at the new path.

  * Move `request_accessibility()` / `request_screen_recording()`
    calls in `gate::run_if_needed` from after the panel to before
    it. These calls have the side effect of registering the calling
    process with TCC — without that, when a user clicks "Open
    System Settings" mid-panel they land in a Privacy & Security
    pane with cua-driver missing from the list. Hoisting the calls
    ahead of presentation closes that UX regression.

  * Phase 2 internals: row construction returns `RowHandles` (icon
    view + title label) stashed in a thread-local so the poll
    callback can update them in place. Heading/subheading text
    rebinds on every grant flip via `setStringValue:`. Auto-dismiss
    triggers via `[NSApp stopModal]` from the poll callback when
    both grants are green. Timer is added to
    `NSModalPanelRunLoopMode` explicitly because
    `[NSApp runModalForWindow:]` does not run timers added to the
    default mode or to NSRunLoopCommonModes.

Tests: 13/13 permissions tests passing
(`cargo test -p platform-macos --lib permissions::`). New parity
tests cover the heading-text matrix and the "subheading mentions
remaining permission" property.

Docs (`docs/.../getting-started/installation.mdx`) rewritten to
describe the new opt-in semantics; CD workflow updated to copy from
`scripts/CuaDriverBundle/`; install-script comments + PARITY.md
point at the new path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion to the previous commit's bundle-skeleton rename. Lands the
Phase 2 panel implementation, the gate-flow restructuring, and the
matching CD/docs/PARITY updates.

  * libs/cua-driver-rs/crates/platform-macos/src/permissions/panel.rs:
    Phase 2 live status rows (RowHandles struct stashed in a thread-
    local so the poll callback can update icon + label color in
    place), dynamic heading/subheading via setStringValue: on every
    tick, auto-dismiss via [NSApp stopModal] when both grants flip
    green. Timer added to NSModalPanelRunLoopMode explicitly because
    [NSApp runModalForWindow:] does NOT fire timers in the default
    mode or NSRunLoopCommonModes. Panel flipped to opt-in:
    panel_enabled() now requires CUA_DRIVER_RS_PERMISSIONS_PANEL set
    to an on-sentinel (1/true/yes/on, case-insensitive); env unset =
    terminal flow.

  * libs/cua-driver-rs/crates/platform-macos/src/permissions/gate.rs:
    Hoist request_accessibility() / request_screen_recording() calls
    from after the panel to before it. These calls have the side
    effect of registering the calling process with TCC; without
    that, clicking "Open System Settings" mid-panel lands the user
    in a Privacy & Security pane where cua-driver is missing from
    the list. New PanelPresentation::ShownAllGranted variant lets
    the gate skip the trailing wait_for_grants loop when the
    panel's poll already saw both grants green.

  * libs/cua-driver-rs/PARITY.md + libs/cua-driver/scripts/_install-rust.sh:
    Update the references to the previous scripts/CuaDriver.app
    skeleton to point at scripts/CuaDriverBundle. Adds a paragraph
    explaining why the path no longer ends in `.app` (LaunchServices
    ghost-bundle prevention).

  * .github/workflows/cd-rust-cua-driver.yml: Copy from
    scripts/CuaDriverBundle/Contents into release/CuaDriver.app/Contents
    instead of from scripts/CuaDriver.app. Comment updated.

  * docs/content/docs/cua-driver/guide/getting-started/installation.mdx:
    Rewrite the permissions-gate Callout to describe the opt-in
    semantics: terminal flow is the documented default; native panel
    is "experimental" and opt-in via CUA_DRIVER_RS_PERMISSIONS_PANEL=1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Building Building Preview, Comment May 18, 2026 4:53pm

Request Review

@f-trycua
f-trycua merged commit 62655a1 into main May 18, 2026
3 of 6 checks passed
@f-trycua
f-trycua deleted the feat/cua-driver-rs-permissions-panel-phases-2-3 branch May 18, 2026 16:53
@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 433b5560-cd42-4a4a-96d0-26a4a07ebf1f

📥 Commits

Reviewing files that changed from the base of the PR and between 2a0dddb and ddad736.

📒 Files selected for processing (9)
  • .github/workflows/cd-rust-cua-driver.yml
  • docs/content/docs/cua-driver/guide/getting-started/installation.mdx
  • libs/cua-driver-rs/PARITY.md
  • libs/cua-driver-rs/crates/platform-macos/src/permissions/gate.rs
  • libs/cua-driver-rs/crates/platform-macos/src/permissions/panel.rs
  • libs/cua-driver-rs/scripts/CuaDriver.app/Contents/MacOS/.gitkeep
  • libs/cua-driver-rs/scripts/CuaDriverBundle/Contents/Info.plist
  • libs/cua-driver-rs/scripts/CuaDriverBundle/Contents/MacOS/.gitkeep
  • libs/cua-driver/scripts/_install-rust.sh

📝 Walkthrough

Walkthrough

The PR refactors the macOS permissions panel from a static modal to a live-updating NSPanel, reorganizes the app bundle skeleton storage to prevent LaunchServices indexing, and updates gate orchestration and documentation. The panel now auto-dismisses when all required grants are active, while the bundle structure moves from .app-suffixed to a non-.app directory.

Changes

macOS Permissions Panel Live-Update and Bundle Structure

Layer / File(s) Summary
App bundle skeleton path reorganization
.github/workflows/cd-rust-cua-driver.yml, libs/cua-driver-rs/PARITY.md, libs/cua-driver-rs/scripts/CuaDriverBundle/Contents/MacOS/.gitkeep, libs/cua-driver/scripts/_install-rust.sh
Bundle source skeleton path changes from scripts/CuaDriver.app/Contents to scripts/CuaDriverBundle/Contents to avoid LaunchServices ghost entries; CD workflow copy command and build/install documentation are updated accordingly.
Permissions panel live-update rewrite
libs/cua-driver-rs/crates/platform-macos/src/permissions/panel.rs
panel.rs converts from static modal to live-updating NSPanel: introduces PanelOutcome::AllGranted, accepts initial_status: PermissionsStatus, switches to opt-in via env_on, and implements AppKit subview construction with polling timer that updates icon/tint states and auto-dismisses when all grants are green.
Gate orchestration with new panel outcome
libs/cua-driver-rs/crates/platform-macos/src/permissions/gate.rs
run_if_needed raises TCC prompts before panel presentation, passes initial status to the panel, introduces PanelPresentation::ShownAllGranted outcome to skip wait_for_grants when panel observes all grants active, and conditions Settings auto-open on panel outcome.
Installation guide permissions gate documentation
docs/content/docs/cua-driver/guide/getting-started/installation.mdx
Clarifies terminal-based gate as default, documents opt-in experimental native panel via CUA_DRIVER_RS_PERMISSIONS_PANEL=1, specifies accepted on-values and bundled .app requirements, and refines CUA_DRIVER_RS_PERMISSIONS_GATE semantics with explicit off-values.

Sequence Diagram(s)

sequenceDiagram
  participant run_if_needed
  participant show_modal_unsafe
  participant pollTick_callback
  participant current_status
  participant NSModalSession

  run_if_needed->>show_modal_unsafe: call with initial PermissionsStatus
  show_modal_unsafe->>NSModalSession: create and run modal loop
  show_modal_unsafe->>pollTick_callback: register timer callback
  loop Poll on timer tick
    pollTick_callback->>current_status: get current grant status
    pollTick_callback->>show_modal_unsafe: update row icons/tints
    pollTick_callback->>show_modal_unsafe: show "All set" strip if all granted
    alt All grants green
      pollTick_callback->>show_modal_unsafe: set AllGranted outcome
      pollTick_callback->>NSModalSession: stop modal
    end
  end
  show_modal_unsafe->>run_if_needed: return PanelOutcome
Loading
sequenceDiagram
  participant run_if_needed
  participant TCC_API
  participant present_panel_if_available
  participant wait_for_grants

  run_if_needed->>TCC_API: raise prompts early (if also_raise_prompts)
  run_if_needed->>present_panel_if_available: call with initial PermissionsStatus
  alt Panel shown and all grants became active
    present_panel_if_available->>run_if_needed: return ShownAllGranted
    run_if_needed->>run_if_needed: skip wait_for_grants loop
  else Panel dismissed or opened settings
    present_panel_if_available->>run_if_needed: return OpenSettings or Dismissed
    run_if_needed->>wait_for_grants: poll for grants
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • trycua/cua#1529: Main PR's gate run_if_needed restructuring and new panel-outcome handling directly integrate with the first-launch permissions gate CLI flow introduced in this PR.
  • trycua/cua#1396: Both PRs refactor macOS Accessibility/Screen Recording prompting: main PR restructures gate to control TCC prompts via also_raise_prompts, while retrieved PR flips check_permissions tool default prompt to true, creating overlap on when TCC dialogs appear.
  • trycua/cua#1562: Main PR reacts to live PermissionsStatus updates in the new panel, while retrieved PR fixes the underlying screen_recording_granted() probe that feeds that status.

🐰 A living panel blooms on macOS screens,
Auto-dismissing when permission's green,
Bundle paths shift to avoid ghost entries' spree,
Gate logic polls and learns to disagree,
What was static now dances with TCC!

✨ 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 feat/cua-driver-rs-permissions-panel-phases-2-3

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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 and usage tips.

f-trycua added a commit that referenced this pull request May 21, 2026
…vas-like surfaces

`click(x, y)` previously routed through `try_invoke_in_window_at_point`
for any UIA element advertising InvokePattern at the click coordinates.
For container surfaces (Pane / Image / Custom / Document / Group)
`Invoke()` fires the element's default action at its centre and ignores
the requested (x, y) — silently breaking pixel precision on canvases,
paint surfaces, image maps, and 3D viewports.

## Repro (from #1621, verified 2026-05-21 on the Windows VM against
   `libs/cua-driver-fixtures/test_page.html` loaded in Edge)

```
cua-driver call click '{"pid":<edge-pid>,"x":110,"y":677}'
# → "✅ Performed UIA Invoke at (110,677)"
# But #canvas-status shows: "canvas: clicked at (152,77)"
#   ─ canvas center, not the requested (110, 677)
```

The canvas's `mousedown` handler fired at synthesised centre coords
because UIA `Invoke()` has no notion of "where inside the element".

## Fix

Add `is_coord_independent_action()` — a control-type whitelist for
elements whose primary action is coord-independent (Button, MenuItem,
Hyperlink, TabItem, ListItem, CheckBox, RadioButton, SplitButton,
TreeItem). For these, UIA Invoke is the semantically correct path —
the element identity *is* the action target, and the click coords
don't matter past hit-testing.

For everything else (Pane / Image / Custom / Document / Group / etc.),
even if the element advertises InvokePattern, fall through to
PostMessage with the literal coords. This preserves UWP / WebView2
coverage for buttons + menu items (the original motivation for the
UIA-first path) without silently rerouting canvas clicks to "click at
centre".

## What this does NOT change

- Element-indexed click (`click(element_index=N)`) is unchanged — it
  takes the UIA Invoke path explicitly via a different code site
  (`impl_.rs:1168`). Callers asking for "invoke this specific element"
  by index keep the previous behaviour.
- Right-click and multi-click already skipped the UIA path
  (`use_uia = (btn == "left" || btn == "middle") && count == 1`); they
  remain on PostMessage.
- The ExpandCollapsePattern preference for Qt menu-bar items (added in
  #1566) is unchanged — that path runs after the new control-type
  filter and only fires when a whitelisted-type element happens to
  also have ExpandCollapse.

## Test plan

- [x] `cargo check -p platform-windows` clean on the VM (41.82s,
      0 new warnings — all 28 warnings are pre-existing)
- [x] `cargo build --release -p cua-driver` clean (24.43s release build)
- [ ] **Runtime verification deferred to next interactive RDP session**:
      load `libs/cua-driver-fixtures/test_page.html` in Edge with the four
      anti-occlusion + a11y flags (see #1620), call `click(pid, x, y)`
      at a non-centre point inside the canvas, expect tool response to say
      `"✅ Posted click to pid <pid>"` (not `"Performed UIA Invoke"`) and
      `#canvas-status` to report the requested coords ±2px. The SSH-only
      session in tonight's autonomous run can't reach an interactive
      desktop (daemon ends up in Session 0; `list_windows` returns empty)
      so the click test couldn't run end-to-end — user needs to RDP in
      to verify.

## Related

- #1620 — Chromium anti-throttling flags (separate fix; needed for any
  Edge/Chrome DOM verification on hidden launches, including this test)

Closes #1621.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
f-trycua added a commit that referenced this pull request May 21, 2026
…vas-like surfaces (#1622)

`click(x, y)` previously routed through `try_invoke_in_window_at_point`
for any UIA element advertising InvokePattern at the click coordinates.
For container surfaces (Pane / Image / Custom / Document / Group)
`Invoke()` fires the element's default action at its centre and ignores
the requested (x, y) — silently breaking pixel precision on canvases,
paint surfaces, image maps, and 3D viewports.

## Repro (from #1621, verified 2026-05-21 on the Windows VM against
   `libs/cua-driver-fixtures/test_page.html` loaded in Edge)

```
cua-driver call click '{"pid":<edge-pid>,"x":110,"y":677}'
# → "✅ Performed UIA Invoke at (110,677)"
# But #canvas-status shows: "canvas: clicked at (152,77)"
#   ─ canvas center, not the requested (110, 677)
```

The canvas's `mousedown` handler fired at synthesised centre coords
because UIA `Invoke()` has no notion of "where inside the element".

## Fix

Add `is_coord_independent_action()` — a control-type whitelist for
elements whose primary action is coord-independent (Button, MenuItem,
Hyperlink, TabItem, ListItem, CheckBox, RadioButton, SplitButton,
TreeItem). For these, UIA Invoke is the semantically correct path —
the element identity *is* the action target, and the click coords
don't matter past hit-testing.

For everything else (Pane / Image / Custom / Document / Group / etc.),
even if the element advertises InvokePattern, fall through to
PostMessage with the literal coords. This preserves UWP / WebView2
coverage for buttons + menu items (the original motivation for the
UIA-first path) without silently rerouting canvas clicks to "click at
centre".

## What this does NOT change

- Element-indexed click (`click(element_index=N)`) is unchanged — it
  takes the UIA Invoke path explicitly via a different code site
  (`impl_.rs:1168`). Callers asking for "invoke this specific element"
  by index keep the previous behaviour.
- Right-click and multi-click already skipped the UIA path
  (`use_uia = (btn == "left" || btn == "middle") && count == 1`); they
  remain on PostMessage.
- The ExpandCollapsePattern preference for Qt menu-bar items (added in
  #1566) is unchanged — that path runs after the new control-type
  filter and only fires when a whitelisted-type element happens to
  also have ExpandCollapse.

## Test plan

- [x] `cargo check -p platform-windows` clean on the VM (41.82s,
      0 new warnings — all 28 warnings are pre-existing)
- [x] `cargo build --release -p cua-driver` clean (24.43s release build)
- [ ] **Runtime verification deferred to next interactive RDP session**:
      load `libs/cua-driver-fixtures/test_page.html` in Edge with the four
      anti-occlusion + a11y flags (see #1620), call `click(pid, x, y)`
      at a non-centre point inside the canvas, expect tool response to say
      `"✅ Posted click to pid <pid>"` (not `"Performed UIA Invoke"`) and
      `#canvas-status` to report the requested coords ±2px. The SSH-only
      session in tonight's autonomous run can't reach an interactive
      desktop (daemon ends up in Session 0; `list_windows` returns empty)
      so the click test couldn't run end-to-end — user needs to RDP in
      to verify.

## Related

- #1620 — Chromium anti-throttling flags (separate fix; needed for any
  Edge/Chrome DOM verification on hidden launches, including this test)

Closes #1621.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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.

1 participant