Skip to content

Fix: first launch never asked for Input Monitoring - #5

Merged
LucaGerlich merged 2 commits into
mainfrom
fix/first-run-permission
Aug 18, 2026
Merged

Fix: first launch never asked for Input Monitoring#5
LucaGerlich merged 2 commits into
mainfrom
fix/first-run-permission

Conversation

@LucaGerlich

Copy link
Copy Markdown
Owner

Reported after installing the 1.0.0 release: the app installs and launches fine, but no permission is ever requested.

Root cause

The launch path only ever checked permission. AppState.initrefreshPermissionStatus()IOHIDCheckAccess, which is a read-only query that by design never prompts. The only call that can raise the system dialog, IOHIDRequestAccess, sat behind the "Request Permission" button in the menu bar menu. The other implicit trigger — opening the HID manager — is guarded by updateKeyboardMonitoringState(), which bails out when permission is missing, so it was never reached either.

With LSUIElement = YES there is no Dock icon and no window, so a first-time user double-clicked the app and saw nothing at all. Worse, an app that never requests access does not appear under Privacy & Security → Input Monitoring, so it could not be granted manually either.

Fix

A first-run window (WelcomeView) that explains what InputPilot does, why it needs Input Monitoring, and that it never reads typed text, with a button that triggers the system prompt and a link to System Settings as a fallback.

It also handles the state macOS leaves behind immediately after a grant, where permission reads as granted but HID access stays blocked until relaunch: the window then says so and offers to quit, instead of leaving the user with an app that looks permitted but does nothing.

Verification

  • Launched the built app with permission missing and queried the window server: the window is on screen at launch (420x442 "Welcome to InputPilot").
  • Two rounds of visual review caught text truncating rather than wrapping; fixed by pinning every multi-line Text against horizontal shrink and capping the actions stack width. Window height grew by exactly one line, confirming the wrap.
  • 3 new unit tests cover the decision logic: onboarding requested when permission is missing, not requested once monitoring runs, and relaunch requested when granted but the monitor cannot start.
  • Full suite green (36 tests).

A new user saw nothing at all on first launch. The launch path only called
IOHIDCheckAccess, which queries permission and never prompts; the only call
that can raise the system dialog, IOHIDRequestAccess, sat behind a button in
the menu bar menu. With LSUIElement there is no Dock icon and no window, so
unless the user thought to open the menu, macOS never asked and the app was
silently inert - and an app that never requests access does not appear in
Privacy & Security either, so it could not be granted manually.

Adds a first-run window explaining what InputPilot does, why it needs Input
Monitoring, and that it never reads typed text, with a button that triggers
the system prompt. It also covers the state macOS leaves behind right after a
grant, where permission reads as granted but HID stays blocked until relaunch,
by offering to quit so the user is not left with an app that looks permitted
but does nothing.

Verified by launching the built app: the window server reports the window
on screen at launch when permission is missing.
Every multi-line Text now refuses horizontal shrink, and the actions stack
gets a width cap so it cannot take its widest child's single-line ideal width
and let the window frame clip it. Without both, SwiftUI picked one text to
truncate: first the caption, then the bullet once the caption was pinned.
Copilot AI lite review requested due to automatic review settings August 18, 2026 18:21
@LucaGerlich
LucaGerlich merged commit 9178122 into main Aug 18, 2026
2 checks passed
@LucaGerlich
LucaGerlich deleted the fix/first-run-permission branch August 18, 2026 18:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a first-launch dead-end for the menu bar app by introducing a first-run Welcome window that explains Input Monitoring, provides a button to trigger the system prompt, and handles the post-grant “relaunch required” state.

Changes:

  • Add a new WelcomeView onboarding window with permission + settings actions and a relaunch flow.
  • Add AppState flags (needsPermissionOnboarding, needsRelaunchAfterGrant) to drive onboarding UI state.
  • Add unit tests covering the onboarding/relaunch decision logic.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
InputPilotTests/AppStateFailurePathTests.swift Adds unit tests validating onboarding/relaunch state decisions.
InputPilot/UI/WelcomeView.swift Introduces the first-run Welcome window UI and actions.
InputPilot/App/InputPilotApp.swift Adds the Welcome window scene and opens it from the menu bar label on first launch.
InputPilot/App/AppState.swift Adds computed flags used to drive onboarding and relaunch messaging.
InputPilot.xcodeproj/project.pbxproj Reorders an existing PBXBuildFile section (no functional change).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +196 to +200
/// macOS usually only lets a process read HID events after a relaunch that
/// follows the grant, so a granted-but-stopped monitor means "restart me".
var needsRelaunchAfterGrant: Bool {
isInputMonitoringGranted && !hidKeyboardMonitor.isRunning
}
Comment on lines +44 to +54
var body: some View {
Image(systemName: "keyboard")
.onAppear {
guard appState.needsPermissionOnboarding else {
return
}

openWindow(id: "welcome")
NSApp.activate(ignoringOtherApps: true)
}
}
Comment on lines +73 to +76
// Without a width cap the stack takes its widest child's ideal
// single-line width, and the window frame clips the caption
// instead of letting it wrap.
.frame(maxWidth: .infinity)
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.

2 participants