User reported three real wizard bugs after the previous round of fixes:
1. Setup… opens a new window every click. The controller's open()
unconditionally constructed a new NSWindow without checking if one
was already on screen. After three clicks of Setup… the user has
three identical wizard windows stacked.
2. No close controls. The first-launch+apiKey branch disabled the
close button by setting .isEnabled = false ONCE at open time, with
no path to re-enable when the user advanced past Stage 1. Result:
stuck-disabled close button for the whole session, even when the
user was on Hotkey stage with no reason to be locked in.
3. Input Monitoring grant invisible to System Settings on ad-hoc
Debug builds. Even after IOHIDRequestAccess fires, the bundle may
not appear in System Settings → Privacy & Security → Input
Monitoring because the ad-hoc signature has TeamIdentifier='not
set' and Apple's TCC daemon cannot pin a stable identity. Without
a hint, the user assumes the wizard is broken when they go to
System Settings looking for Jarvis.
Fixes:
- OnboardingWizardController.open():
* If self.window already exists and is visible: makeKeyAndOrderFront +
NSApp.activate; do NOT spawn a new window.
* styleMask always [.titled, .closable, .miniaturizable] — the close
button is now governed by an enable/disable helper rather than the
style mask, so the chrome stays consistent.
* Subscribe to state.objectWillChange (Combine) and re-run
applyCloseButtonState whenever the wizard advances; the close
button auto-enables once apiKey is stored.
* applyCloseButtonState lock condition is now firstLaunch && stage ==
.apiKey && !apiKeyStored — the moment the key lands in Keychain, the
lock releases. Window level drops back to .normal too.
- WizardStageTCCView Input Monitoring row:
* Added a 'Re-check' button after first probe so user can re-query
after granting in System Settings without restarting the wizard.
* Added a System-Settings helper card explaining what to do when the
bundle isn't auto-listed (the macOS quirk for ad-hoc signed Debug
builds): 'click + and choose Jarvis.app from your build folder'.
* Granted check-mark wins over Re-check button — once granted, only
the green ✓ shows.
Verified: check-app-builds clean. dev-reset.sh + relaunch puts wizard
at apiKey with close button correctly disabled; entering API key
re-enables close; advancing through stages keeps it enabled.
Phase 1 REVIEW.md WR-09: `OnboardingWizardController.open` called
`state.refresh()` immediately before building the hosting view. The
refresh is synchronous and @MainActor-safe, so the potential race
(refresh publish arriving after SwiftUI subscribes) doesn't occur in
practice — but the sequencing was an implicit invariant worth
codifying.
App/Wizard/OnboardingWizardController.swift
- Add docstring noting why refresh must complete before view
construction (keychain-lock contention latency + publish ordering).
- Add runtime assertion `Thread.isMainThread` after refresh — makes
the invariant explicit and fails loudly if a future refactor moves
refresh off-main.
Notes for future work:
- A truly async-off-main refresh would make the UX WORSE (user sees
a flash of empty wizard for the async duration). Current posture
of "run refresh synchronously, short-lived main-thread block" is
correct for this scope; revisit only if the Keychain contention
stalls become user-visible in practice.