Skip to content

feat(cua-driver-rs): Phase 1 native NSPanel onboarding gate (macOS) - #1565

Merged
f-trycua merged 1 commit into
mainfrom
feat/cua-driver-rs-permissions-panel-phase1
May 18, 2026
Merged

feat(cua-driver-rs): Phase 1 native NSPanel onboarding gate (macOS)#1565
f-trycua merged 1 commit into
mainfrom
feat/cua-driver-rs-permissions-panel-phase1

Conversation

@f-trycua

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

Copy link
Copy Markdown
Collaborator

Summary

Replaces the terminal-only first-launch permissions banner with a small native CuaDriver Permissions window when the daemon is launched from the bundled /Applications/CuaDriver.app. Closes the most user-visible parity gap with the Swift driver's onboarding flow.

Phase 1 of a planned three-phase rollout (see planning agent's report — Phase 2 = live status rows + dynamic heading + auto-dismiss; Phase 3 = chaining + always-present + ready strip).

What Phase 1 ships

  • crates/platform-macos/src/permissions/panel.rs (~410 LOC, new) — show_modal(opts) -> PanelOutcome that builds an NSPanel (.titled + .closable, level .floating, 460×280) listing the missing TCC grants with two buttons:

    • Open System Settings (default; Return key) — opens the matching Privacy panes via the existing open_system_settings_for path.
    • Continue anyway — dismisses; the gate still polls for grants.
    • Red-dot close = Continue anyway.

    Raw `objc2 + msg_send!` patterns matching `cursor/overlay.rs`. No new dependencies, no new objc2-app-kit feature flags. Custom `CuaDriverPermissionsPanelTarget` ObjC class registered lazily and memoised in a `OnceLock` so the second invocation in the same process reuses the registration.

  • `gate.rs`: `run_if_needed` is now a router. When `panel::panel_enabled()` returns true (main thread + running inside an `.app` bundle + env-var opt-out not set) it calls `panel::show_modal` instead of printing the terminal banner. Post-panel Settings-open is driven by the user's button choice — Open Settings opens the panes; Continue anyway suppresses the auto-open. Wait-for-grants polling runs in both cases.

  • `mod.rs`: adds `#[cfg(target_os = "macos")] pub mod panel;` and updates the doc-comment to describe the dual presentation surfaces.

What Phase 1 does NOT ship (deferred to Phase 2+)

  • Live red/green status icons that update as grants flip
  • Dynamic heading ("needs your permission" → "One more permission" → "ready")
  • Auto-dismiss when both grants flip green
  • Smart Settings-pane chaining (auto-open second pane on first-grant flip)
  • Hover effects, gear affordances, and "All set" pill

Threading

The Serve arm in `cua-driver/src/main.rs` already calls the gate synchronously on the main thread before any worker threads spawn — and before `NSApp.run()` is called by the cursor overlay (which only runs in the fall-through MCP arm, after Serve has returned). So `panel::show_modal` can claim the main thread, run `[NSApp runModalForWindow:]`, and return without colliding with the overlay's AppKit loop. `MainThreadMarker::new().expect(...)` panics loudly if anyone wires this up wrong in the future.

Fallback paths (zero regression on these)

The terminal banner + auto-Settings-open path remains in place and is selected when ANY of:

  • `CUA_DRIVER_RS_PERMISSIONS_PANEL` env var is `0`/`false`/`no`/`off` (case-insensitive)
  • Not running on the main thread
  • Not running inside an `.app` bundle (`std::env::current_exe()` path doesn't contain a `.app` component) — keeps `./target/release/cua-driver serve` on the terminal flow
  • The full gate is opted out via `--no-permissions-gate` / `CUA_DRIVER_RS_PERMISSIONS_GATE=0`

CI / headless runners pick the terminal path automatically without any flag.

Live test on this Mac

  1. `cp target/release/cua-driver /Applications/CuaDriver.app/Contents/MacOS/cua-driver`
  2. `tccutil reset Accessibility com.trycua.driver && tccutil reset ScreenCapture com.trycua.driver`
  3. `open -n -g -a /Applications/CuaDriver.app --args serve`

Result: panel rendered (System Events confirmed `CuaDriver Permissions` window owned by the daemon), both buttons fired their correct outcomes, daemon proceeded past the gate after the user acted on the panel.

Tests

  • 11/11 permissions tests passing (`cargo test -p platform-macos --lib permissions::`)
  • New env-var-parsing tests share a process-global mutex with the existing gate tests to avoid `set_var`/`remove_var` races under parallel `cargo test` runs.

Docs

Updated `docs/content/docs/cua-driver/guide/getting-started/installation.mdx` Callout to document the new panel UI, the `CUA_DRIVER_RS_PERMISSIONS_PANEL` opt-out env var, and the bare-binary / headless fallback behaviour.

Test plan

  • `cargo test -p platform-macos --lib permissions::` — 11/11 green
  • `cargo build --release -p cua-driver --bin cua-driver` — clean
  • Live test: panel renders, buttons work, gate completes
  • Verify panel does NOT show on bare-binary `./target/release/cua-driver serve` (terminal flow only)
  • Verify `CUA_DRIVER_RS_PERMISSIONS_PANEL=0` falls back to terminal flow even inside the .app
  • Verify CI macOS lane (no display attached) keeps using terminal flow without panic

Related

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a native macOS modal dialog that appears when permissions are missing, with options to open System Settings or continue without permissions.
    • Added environment variable support to disable the permissions panel for CI/headless environments.
  • Documentation

    • Updated permissions gate documentation with improved guidance for headless and CI scenarios, plus clarified environment variable configuration options.

Review Change Stack

Replaces the terminal-only first-launch permissions banner with a small
native `CuaDriver Permissions` window when the daemon is launched from
the bundled `/Applications/CuaDriver.app`. Closes the most user-visible
parity gap with the Swift driver's onboarding flow (issue #1561 + the
follow-up "no window on Rust port" report).

What this PR ships (Phase 1 only):

  * A 460x280 NSPanel (`.titled`, `.closable`, level `.floating`) built
    with raw `objc2 + msg_send!` patterns matching `cursor/overlay.rs`.
    No new dependencies; no new objc2-app-kit feature flags. The panel
    lists the missing TCC grants and offers two buttons:
      - Open System Settings (default action, opens the matching
        Privacy panes via the existing `open_system_settings_for`
        path)
      - Continue anyway (dismisses; gate still polls for grants)
    Red-dot close is treated as Continue anyway.

  * A custom `CuaDriverPermissionsPanelTarget` ObjC class registered
    lazily at first call and memoised in a `OnceLock` so the second
    panel invocation in the same process reuses the registration.

  * `gate::run_if_needed` becomes a router. When `panel::panel_enabled()`
    returns true (main thread, running inside an `.app` bundle, env-var
    opt-out not set), it calls `panel::show_modal` instead of printing
    the terminal banner. Bare-binary launches, headless environments,
    CI, and explicit `CUA_DRIVER_RS_PERMISSIONS_PANEL=0` keep the
    existing banner + polling flow. Zero regression on those paths.

  * The post-panel Settings-open is now driven by the user's button
    choice — Open Settings opens the panes; Continue anyway suppresses
    the auto-open. Wait-for-grants polling runs in both cases since the
    user can grant in their own time without the panel's help.

Threading: the Serve arm in `cua-driver/src/main.rs` already calls the
gate synchronously on the main thread before any worker threads spawn
or NSApp.run() is called by the cursor overlay (which only runs in the
fall-through MCP arm). `panel::show_modal` asserts `MainThreadMarker`
and uses `[NSApp runModalForWindow:]` so the gate blocks the main
thread cleanly and returns control before the daemon socket binds.

What this PR does NOT ship (deliberately deferred to Phase 2+):
  * Live red/green status icons that update as grants flip.
  * Dynamic heading ("needs your permission" → "One more permission"
    → "ready").
  * Auto-dismiss when both grants flip green.
  * Smart Settings-pane chaining (open second pane when first grant
    flips).
  * Hover effects, gear affordances, and "All set" pill.

Tested live on this Mac: installed the Phase 1 binary into
`/Applications/CuaDriver.app`, ran `tccutil reset` for both grants,
launched via `open -n -g -a /Applications/CuaDriver.app --args serve`.
Panel rendered as expected (System Events confirmed a `CuaDriver
Permissions` window owned by the daemon); both buttons fired their
correct outcomes; the daemon proceeded past the gate after the user
acted on the panel.

11 of 11 permissions tests passing (`cargo test -p platform-macos --lib
permissions::`); env-var parsing tests share a process-global mutex
with the existing gate tests to avoid `set_var`/`remove_var` races
under parallel `cargo test` runs.

Docs updated: `docs/content/docs/cua-driver/guide/getting-started/
installation.mdx` Callout documents the new panel UI, the
`CUA_DRIVER_RS_PERMISSIONS_PANEL` opt-out env var, and the
bare-binary / headless fallback behaviour.

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 Ready Ready Preview, Comment May 18, 2026 4:25pm

Request Review

@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: d5fb3192-e968-413d-90df-4a7ffd8b8eea

📥 Commits

Reviewing files that changed from the base of the PR and between e00840a and 6b5a9b6.

📒 Files selected for processing (4)
  • docs/content/docs/cua-driver/guide/getting-started/installation.mdx
  • libs/cua-driver-rs/crates/platform-macos/src/permissions/gate.rs
  • libs/cua-driver-rs/crates/platform-macos/src/permissions/mod.rs
  • libs/cua-driver-rs/crates/platform-macos/src/permissions/panel.rs

📝 Walkthrough

Walkthrough

This PR adds a native macOS AppKit permissions panel for driver startup, with public types and gating, full modal UI implementation, integration into the existing permissions gate, and documentation of the interactive flow and fallback behavior.

Changes

Permissions Panel Implementation

Layer / File(s) Summary
Panel API contract and gating
libs/cua-driver-rs/crates/platform-macos/src/permissions/panel.rs, libs/cua-driver-rs/crates/platform-macos/src/permissions/mod.rs
Defines public PanelOutcome and PanelOpts types; panel_enabled() checks environment opt-out, main-thread requirement, and .app-bundle path; show_modal() enforces threading and delegates to unsafe implementation. Module is exposed and wired into the permissions module with updated documentation.
Panel UI implementation
libs/cua-driver-rs/crates/platform-macos/src/permissions/panel.rs
show_modal_unsafe constructs and configures an NSPanel with fixed geometry, heading/subheading, capped missing-permission list, and two buttons ("Continue anyway" and "Open System Settings"). Button actions are wired via a lazily-registered Objective-C runtime class that writes outcomes to a thread-local cell and stops the modal loop. UI helpers create styled NSString, NSTextField, and NSButton instances.
Gate integration
libs/cua-driver-rs/crates/platform-macos/src/permissions/gate.rs
run_if_needed now attempts modal panel presentation and matches the outcome to conditionally print the terminal banner and auto-open System Settings. Introduces private PanelPresentation enum and present_panel_if_available helper that returns NotShown on non-macOS platforms.
User documentation
docs/content/docs/cua-driver/guide/getting-started/installation.mdx
Documents the interactive native permissions window flow with button actions, the headless/terminal-only fallback, environment-variable opt-out via CUA_DRIVER_RS_PERMISSIONS_PANEL=0, and clarified case-insensitive sentinel values for disabling the permissions gate.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • trycua/cua#1529: This PR extends the existing macOS permissions-gate implementation introduced in the previous Rust port migration by adding a new native panel UI surface and refactoring the gate orchestration.

Poem

🐰 A modal springs to life on macOS screens,
With buttons bright and AppKit means,
"Open Settings" or "Continue on" we say,
The driver asks for permission—button by button, day by day! 🎯

✨ 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-phase1

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
f-trycua merged commit 2a0dddb into main May 18, 2026
5 of 7 checks passed
@f-trycua
f-trycua deleted the feat/cua-driver-rs-permissions-panel-phase1 branch May 18, 2026 16:25
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