Skip to content

feat(macos): first-run permissions window - #735

Merged
EtienneLescot merged 3 commits into
mainfrom
feat/macos-permissions-onboarding
Sep 23, 2026
Merged

EtienneLescot merged 3 commits into
mainfrom
feat/macos-permissions-onboarding

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

Why

On a fresh Mac, OpenScreen could not get Screen Recording through its own UI:

  • The app only raised macOS' prompt for the status not-determined, which Chromium never reports: getMediaAccessStatus("screen") is CGPreflightScreenCaptureAccess(), a bool. So the first Record click went straight to an English "Open System Settings" dialog, and OpenScreen did not even appear in the list there yet. Credit to @heyitsR1 for this diagnosis in fix(macos): raise the Screen Recording prompt on first run #302.
  • The app's own read is cached for the life of the process, so it never saw a grant the user made while the app was running.
  • The microphone was requested on every launch, before anything used it.

This PR replaces all of that with one permissions window. It supersedes #302.

What the user sees

At launch, while Screen Recording is missing, a window lists:

Permission Level Why
Screen & system audio Required ScreenCaptureKit captures system audio too, so this covers it
Accessibility Recommended The cursor helper uses it to tell pointer and text cursors apart
Microphone, Camera Optional Still requested at the moment of use if skipped here

Each row shows its live status and the one action that can change it:

  • The first time, the button raises macOS' prompt. For Screen Recording it is labelled Continue, not "Allow", because macOS' prompt has no Allow button, only a way into System Settings.
  • After that, the button opens the right System Settings pane, with help for when OpenScreen is missing from the list.

"Get started" unlocks once Screen Recording is granted. If the grant exists but this process cannot use it yet, the window offers Restart OpenScreen. After a relaunch mid-onboarding (our button, or System Settings' own "Quit & Reopen"), the window comes back to finish the remaining rows. It does not reappear once it has been closed with the grant in hand, and it never appears for users who already held the grant.

The window is also reachable from the app menu and the tray ("Permissions…"). The Record button and the editable-cursor check open it instead of the two hard-coded English dialogs.

How

  • electron/permissions/macPermissions.ts holds the pure logic (statuses, requests, when to show at launch), with injected dependencies and unit tests.
  • Screen Recording is read from a helper spawned for each read (openscreen-screencapturekit-helper --screen-access-status, from fix(macos): raise the Screen Recording prompt on first run #302). A fresh process has no stale cache.
  • "Never asked" vs "refused": macOS reports both as the same bool. The app keeps its own note of the prompts it raised (userData/permissions.json), as Kap and Cap do.
  • The prompt is raised in-process (Chromium's desktopCapturer), so TCC files the grant under the app bundle.
  • Settings links use the legacy com.apple.preference.security?Privacy_* form, which opens the right pane from macOS 13 to 26.
  • On macOS 15+, the window warns about the second macOS alert before the first recording meets it: "…is requesting to bypass the system private window picker…". Every ScreenCaptureKit app outside Apple's own picker gets it, and it comes back periodically. The window describes it, says to click Allow, and says it returns. It is an explanation, not a row: tested on 26.5, replayd shows it on its own schedule whatever the last answer was (so the app cannot raise it on demand), and its approval store is unreadable (so there is no status to show). Removing it for good would take SCContentSharingPicker, which is being studied for the follow-up.

Removed: the launch-time microphone request, the renderer's 6 s retry loop (openSourceSelectorFlow), the request-screen-access channel, and both English dialogs.

Strings: all 15 locales.

Verified

Packaged, Developer-ID-signed build on macOS 26.5, fresh profile, TCC reset beforehand:

  • First launch: the window opens by itself, and no microphone prompt appears
  • Continue → macOS prompt → toggle in Settings → "Later": the row turns Allowed live, and Restart is offered
  • Restart: the window comes back with Screen Recording granted and usable
  • Microphone and Accessibility granted from the window: their rows update live, without a restart
  • Get started, then relaunch: the window stays away, and Record opens the source picker directly
  • vitest (full suite), tsc, biome, docs:check, i18n-check

Not verified on macOS 13, 14 or 15. The per-version behaviour (deep links, prompt wording, the 15+ reminder) relies on Apple's documentation and public reports.

Follow-up

A second PR will let the ScreenCaptureKit helper list sources for the picker. A grant then works immediately, and the Restart step disappears except as a fallback.

Summary by CodeRabbit

  • New Features
    • Added a macOS permissions window to check Screen Recording, Accessibility, microphone, and camera access, request permissions, and open the relevant settings.
    • The app can prompt for required permissions at startup and offers a menu option to reopen the permissions window.
    • Added translated permissions setup guidance across supported languages.
  • Bug Fixes
    • Improved Screen Recording permission checks so newly granted access is detected, with a restart option when needed.

A fresh macOS install could not reach Screen Recording: the app only
raised the prompt for a status Chromium never reports (`not-determined`),
so the first Record click went straight to an English 'Open System
Settings' dialog for an app that was not even in the list yet. The
microphone was also requested on every launch, before anything used it.

A permissions window now opens at launch while Screen Recording is
missing, and from the app and tray menus. It lists Screen & system
audio (required), Accessibility (recommended, for the cursor), and the
microphone and camera (optional), each with its live status and the one
action that can move it: raise macOS' prompt the first time, open the
right System Settings pane after that.

- Screen Recording is read from a helper spawned per read
  (`--screen-access-status`): the app's own read is cached for the life
  of the process and never sees a grant made while it runs.
- 'Never asked' and 'refused' are the same bool on macOS, so the app
  keeps its own note of the prompts it raised (permissions.json).
- When the grant exists but this process cannot use it yet, the window
  offers the relaunch.
- The Record button and the cursor check open the window instead of the
  two hard-coded English dialogs; the renderer's 6 s retry loop and the
  launch-time microphone request are gone.

Strings in all 15 locales.
System Settings offers 'Quit & Reopen' the moment Screen Recording is
turned on, and the window tells the user it comes back from that. It
did not: at launch it only opened while Screen Recording was missing,
so the relaunch left Accessibility, the microphone and the camera
behind, and broke the window's own promise.

The window now also opens at launch when the onboarding was started
(its prompt raised) and not finished. It counts as finished once it is
closed with Screen Recording granted. Users who held the grant before
the window existed never raised a prompt through it, so an update does
not show it to them.
@coderabbitai

coderabbitai Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: b831d001-d149-459f-a3fd-bfe4b1b79009

📥 Commits

Reviewing files that changed from the base of the PR and between 94e6cff and e94d78c.

📒 Files selected for processing (49)
  • electron/electron-env.d.ts
  • electron/ipc/handlers.ts
  • electron/main.ts
  • electron/native-bridge/screen/macScreenAccess.test.ts
  • electron/native-bridge/screen/macScreenAccess.ts
  • electron/native/README.md
  • electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/ScreenCaptureRecorder.swift
  • electron/permissions/index.ts
  • electron/permissions/macPermissions.test.ts
  • electron/permissions/macPermissions.ts
  • electron/preload.ts
  • electron/windows.ts
  • src/App.tsx
  • src/components/launch/LaunchWindow.test.tsx
  • src/components/launch/LaunchWindow.tsx
  • src/components/launch/openSourceSelectorFlow.test.ts
  • src/components/launch/openSourceSelectorFlow.ts
  • src/components/permissions/PermissionsWindow.test.tsx
  • src/components/permissions/PermissionsWindow.tsx
  • src/i18n/locales/ar/common.json
  • src/i18n/locales/ar/launch.json
  • src/i18n/locales/cs/common.json
  • src/i18n/locales/cs/launch.json
  • src/i18n/locales/de/common.json
  • src/i18n/locales/de/launch.json
  • src/i18n/locales/en/common.json
  • src/i18n/locales/en/launch.json
  • src/i18n/locales/es/common.json
  • src/i18n/locales/es/launch.json
  • src/i18n/locales/fr/common.json
  • src/i18n/locales/fr/launch.json
  • src/i18n/locales/it/common.json
  • src/i18n/locales/it/launch.json
  • src/i18n/locales/ja-JP/common.json
  • src/i18n/locales/ja-JP/launch.json
  • src/i18n/locales/ko-KR/common.json
  • src/i18n/locales/ko-KR/launch.json
  • src/i18n/locales/pt-BR/common.json
  • src/i18n/locales/pt-BR/launch.json
  • src/i18n/locales/ru/common.json
  • src/i18n/locales/ru/launch.json
  • src/i18n/locales/tr/common.json
  • src/i18n/locales/tr/launch.json
  • src/i18n/locales/vi/common.json
  • src/i18n/locales/vi/launch.json
  • src/i18n/locales/zh-CN/common.json
  • src/i18n/locales/zh-CN/launch.json
  • src/i18n/locales/zh-TW/common.json
  • src/i18n/locales/zh-TW/launch.json
💤 Files with no reviewable changes (3)
  • src/components/launch/LaunchWindow.test.tsx
  • src/components/launch/openSourceSelectorFlow.test.ts
  • src/components/launch/openSourceSelectorFlow.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The change adds a macOS permissions manager, a dedicated permissions window, and a renderer interface for checking and requesting Screen Recording, Accessibility, microphone, and camera permissions. It also adds a fresh-process Screen Recording probe, updates app entry points, and adds translations.

Changes

macOS Permissions Onboarding

Layer / File(s) Summary
Screen Recording probe
electron/native-bridge/screen/macScreenAccess.ts, electron/native-bridge/screen/macScreenAccess.test.ts, electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/ScreenCaptureRecorder.swift, electron/native/README.md
Adds a helper mode that reports Screen Recording status and an Electron probe that reads it in a fresh process. Tests cover probe responses, failures, timeouts, and non-macOS behavior.
Permission model and lifecycle
electron/permissions/macPermissions.ts, electron/permissions/macPermissions.test.ts
Adds permission snapshot, request, settings, and onboarding lifecycle rules. Tests cover status mapping, permission requests, settings URLs, and launch visibility.
Electron permissions integration
electron/permissions/index.ts, electron/ipc/handlers.ts, electron/main.ts, electron/preload.ts, electron/electron-env.d.ts, electron/windows.ts
Adds persisted permission history, IPC operations, and permissions-window management. App startup and macOS menus connect to the window. Screen and accessibility requirements route through the shared permissions flow.
Permissions window and renderer updates
src/components/permissions/*, src/App.tsx, src/components/launch/*, src/i18n/locales/*/{common,launch}.json
Adds a polling permissions interface with status-aware actions and relaunch or start controls. Renders the permissions window, removes the launch-screen retry flow, and adds UI tests and translations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PermissionsWindow
  participant Preload
  participant PermissionsIPC
  participant MacPermissions
  participant ScreenAccessProbe
  PermissionsWindow->>Preload: permissions.get()
  Preload->>PermissionsIPC: permissions:get
  PermissionsIPC->>MacPermissions: read()
  MacPermissions->>ScreenAccessProbe: readMacScreenCaptureAccess()
  ScreenAccessProbe-->>MacPermissions: Screen Recording result
  MacPermissions-->>PermissionsIPC: permissions snapshot
  PermissionsIPC-->>Preload: permissions snapshot
  Preload-->>PermissionsWindow: permissions snapshot
Loading

Suggested reviewers: my-denia

Merge Risk: ⚪ Minimal · up to e94d7

The change adds a macOS permissions onboarding window and moves the Screen Recording and Accessibility prompts into it. No concrete defect was found in the permission reads, the IPC flow, or the launch behavior. The helper used to read Screen Recording status is packaged where the app looks for it. The change looks ready to merge, with normal testing on older macOS versions.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.11% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 36 functions across 15 files. (31 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: a first-run permissions window for macOS.
Description check ✅ Passed The description is detailed and covers the change, user behavior, implementation, testing, platform scope, and related issue context. It does not follow the repository template headings exactly and om…
Full details: Docstring Coverage

Explanation

Docstring coverage is 36.11% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 36 functions across 15 files. (31 skipped: 31 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

…lert needs

From macOS 15 every ScreenCaptureKit app outside Apple's picker gets an
alert asking whether it may bypass the system private window picker,
again from time to time. The window's line about it ('macOS will
occasionally ask you to confirm this access') was too vague for anyone
to connect it with that alert when it arrived.

The line now describes the alert, says to click Allow, and says it
comes back. It stays an explanation rather than a row: replayd shows the
alert on its own schedule whatever the last answer was, and its approval
is unreadable, so there is nothing to trigger or to show a status for.
@EtienneLescot
EtienneLescot merged commit f418465 into main Sep 23, 2026
20 checks passed
@EtienneLescot
EtienneLescot deleted the feat/macos-permissions-onboarding branch September 23, 2026 21:21
EtienneLescot added a commit that referenced this pull request Sep 23, 2026
On macOS an owned message box is a sheet, and AppKit dims the whole
owning window behind it. The HUD is a ~907x696 transparent window around
a ~60px bar, so every dialog it owned greyed a large invisible area of
the desktop.

On macOS the transparent overlays (HUD, source selector, countdown) no
longer own their message boxes: an unowned alert is app-modal and sits
above their floating level. Windows and Linux keep the owner, where an
unowned dialog opens behind the always-on-top HUD.

The two permission dialogs this first covered in ipc/handlers.ts are
gone since #735 replaced them with the permissions window; what remains
are main.ts's dialogs (About, updates, diagnostics), all of which fall
back to the HUD as their owner.
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