Commit Graph
3 Commits
Author SHA1 Message Date
james.maes 4b5f535d86 fix(wizard): single-window reuse + always-closable + Re-check + System-Settings-fallback
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.
2026-05-02 07:41:15 -05:00
james.maes 1fc613bcfa fix(01-WR-09): document refresh-before-view-build invariant in wizard open
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.
2026-04-23 16:27:25 -05:00
james.maesandClaude Opus 4.7 ebefb91043 feat(01-04): build first-launch wizard + full AppDelegate launch wiring
- AnthropicKeyValidator: local sk-ant- prefix check + URLSession-injectable
  GET to api.anthropic.com/v1/models; returns typed enum, never logs key
- WizardState: @MainActor ObservableObject; D-09 stateless re-entry via
  refresh(); firstUnresolvedStage advances apiKey → tcc → hotkey → complete
- WizardStageAPIKeyView: SecureField, validator + Keychain round-trip; key
  value lives only in Keychain (SEC-01)
- WizardStageTCCView: Input Monitoring row actively probes; Mic/Camera/
  Automation rows are explainer-only (D-08/SEC-04); no permission APIs
  called from the view
- WizardStageHotkeyView: ShortcutRecorderView + CollisionDetector wiring;
  Skip leaves hotkey unbound (SHELL-02)
- WizardView: three-stage root with header dots + Back footer
- OnboardingWizardController: NSWindow hosting; first-launch .modalPanel
  + close-button disabled until API key stored (D-06)
- AppDelegate: full 8-step launch chain — logging bootstrap (S-8) →
  entitlement gate (Plan 03) → config load (NSAlert on malformed, D-19/S-6) →
  menu bar + HUD + banner install → Keychain fetch (.keychainEmpty banner
  on itemNotFound) → Input Monitoring probe (.inputMonitoringDenied banner
  on denial, SHELL-06) → HotkeyBinder init → Wizard open on first launch.
  copyStateDump writes boolean-only presence; API key VALUE never clipboarded
- JarvisApp: Settings scene stub; real NSPanels managed by AppDelegate
- Tests: 11 new (WizardState 4, AnthropicKeyValidator 3, WizardTCC 1,
  AppDelegateWiring 4) + updated AppDelegateEntitlementTests for the new
  launch chain; all 25 JarvisAppTests pass
- Re-inserted PBXCopyFilesBuildPhase for Contents/Helpers per Plan 01/03
  precedent (xcodegen strips empty copyFiles)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 14:07:04 -05:00