Commit Graph
5 Commits
Author SHA1 Message Date
james.maesandClaude Opus 4.7 67748f5336 feat(round-3): Status… menu item + NSPanel — live re-probe on open
New AppKit panel (NSPanel + SwiftUI hosting view) renders the live
BootHealthSnapshot, re-probes on every appearance, and offers
"Re-probe all" + "Copy report" buttons. Operator/dev tool, not a
user-facing surface — AppKit deliberately so it doesn't ride the
R3F render pipeline.

Anti-fake-status carries forward from Round 2: the panel's `.task`
modifier fires a fresh `runBootHealth()` sweep on every open, never
showing a cached boot snapshot. Cmd+R re-probes; Cmd+Shift+C copies.

Non-modal: NSPanel with [.titled, .closable, .resizable,
.nonactivatingPanel] + makeKeyAndOrderFront(_:) — no runModal calls.
scripts/check-no-modal-presentation.sh stays green.
isReleasedWhenClosed=false so the user can close and re-open without
re-creating the model (preserves "Copied X B" caption + last
snapshot across opens, in addition to surviving the AppKit
"window deallocs on close" default that would otherwise crash on
second open).

Files:
  - App/MenuBar/StatusPanel.swift (new):
    * StatusPanelModel — @MainActor ObservableObject, owns the
      snapshot + isProbing + copyConfirmation @Published state.
      `probeRunner` closure injected so "Re-probe all" hits the
      production AppDelegate.runBootHealth() path (single source
      of truth — panel + boot use the same code).
    * StatusPanelView — SwiftUI body. Scrollable rows, header
      with spinner during probe, footer with re-probe + copy
      buttons + caption.
    * SubsystemRow (private) — glyph (✓/⚠/✗/?), name, status
      label, latency, evidence, reason. Colors per status
      (green/orange/red/gray). Multi-line evidence + reason.
    * StatusPanel: NSPanel — hosts the SwiftUI view, `present()`
      method centers + brings front + activates app.
  - App/MenuBar/MenuBarContextMenu.swift:
    * `build` now takes a `statusAction:` closure (5th param).
    * "Status…" NSMenuItem between "Settings…" and "Show Dev Overlay".
  - App/AppDelegate.swift:
    * statusPanel: StatusPanel? lazy property.
    * openStatusPanel() method — lazy-creates panel on first click,
      passes self.runBootHealth as the probe runner closure.
    * installMenuBar's MenuBarContextMenu.build call updated for
      the new param.
  - Jarvis.xcodeproj/project.pbxproj: PBXBuildFile + PBXFileReference
    for StatusPanel.swift, added to MenuBar PBXGroup + Sources phase.

Verification:
  - bash scripts/check-app-builds.sh: PASS
  - bash scripts/check-no-modal-presentation.sh: OK
  - bash scripts/check-install-order.sh: PASS
  - bash scripts/check-no-leftover-stubs.sh: PASS

Dogfooding next: cold-launch → right-click menu bar icon → Status…,
verify rows render with concrete evidence (mic device name, model
ID list, etc.); click Re-probe and confirm rows refresh with new
latency values; click Copy report and paste into a text editor to
verify JSON is well-formed and complete.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:12:37 -05:00
james.maes 7043b3bd6e feat(settings): real Settings panel — single window, freeform editing of API key + Input Monitoring + hotkey
Phase A3 of AUDIT-AND-FIX-PLAN.md. Replaces the SwiftUI `Settings { Text("Settings — coming soon.") }` placeholder and the menu-bar Settings… → wizard stopgap (commits c7242ed + c75ea9b) with a real `SettingsView` hosted in an `NSWindow`.

Architecture mirrors `OnboardingWizardController`:

  Setup…    → `OnboardingWizardController`  (sequential first-run flow, terminal Save)
  Settings… → `SettingsWindowController`    (freeform panel, every row independently editable)

Both surfaces share the same `WizardState`, so canonical truth lives in
one place:
  • API key            → Keychain (`Keychain.set(.anthropic, …)`)
  • Input Monitoring   → IOHIDCheckAccess (queried on each refresh)
  • Global hotkey      → UserDefaults `Jarvis.hotkey`

Edits in either surface write through to those stores, so re-opening the
other surface reflects the change immediately. Window discipline matches
the wizard: a single reusable `NSWindow`; clicking Settings… repeatedly
brings the existing window forward and `state.refresh()`-es rather than
stacking new windows.

Hotkey-edit hot-reload: `SettingsView.onHotkeyChanged` flows to
`AppDelegate.openSettings`'s closure, which calls `bindHotkeyFromWizard`
(reused from the wizard path). The new shortcut takes effect immediately
without a relaunch.

Files added:
  • App/Settings/SettingsView.swift (was already in tree as untracked WIP;
    296 LOC, three sections: API key / Permissions / Hotkey)
  • App/Settings/SettingsWindowController.swift (new, 70 LOC; window
    lifecycle + dependency injection)

Files modified:
  • App/AppDelegate.swift — `settingsWindowController` property,
    instantiation alongside `wizardController`, new `openSettings()`
    private helper, menu wiring change
    (`settingsAction: { ... openSettings() }` instead of `openWizard`).
  • App/JarvisApp.swift — replaced the "coming soon" Text body of the
    SwiftUI `Settings` scene with `EmptyView()`. The scene exists only
    to satisfy the `App` protocol's Scene requirement; with
    LSUIElement=YES the auto-generated app-menu Settings… is never
    user-visible. AppDelegate manages the real Settings panel via
    `SettingsWindowController`.
  • App/MenuBar/MenuBarContextMenu.swift — comment update describing
    the new Settings… semantics (no longer points at the wizard).
  • Jarvis.xcodeproj/project.pbxproj — added Settings/ PBXGroup with
    the two new files, PBXBuildFile entries for both, and group
    reference under the App PBXGroup's children. Sources phase
    references both build files. UUIDs generated via `uuidgen`.

Verification:
  • `scripts/check-app-builds.sh`: PASS — App target compiles cleanly
    with the new Settings/ group and new files.
  • SwiftUI/AppKit hosting: `NSHostingController(rootView: SettingsView)`
    matches the existing wizard host pattern.
  • No new test regressions expected (no tests touch Settings yet —
    test coverage is a Phase D-Phase-3 / Phase E line item).

Refs: .planning/AUDIT-AND-FIX-PLAN.md Phase A3, Phase C2 ("resolved by
Phase A3"). Closes the C2 finding; F-A1-01 (HUD rings static) and
F-A2-01 (stale `JarvisBusWorld` test) remain open in
.planning/AUDIT-FINDINGS.md.
2026-05-02 19:39:30 -05:00
james.maes 683be70ae6 fix(hud,menu): summon brings HUD to front + Settings menu opens wizard
Two surgical UX fixes before the broader audit:

1. JarvisHUDPanel.summon() used orderFrontRegardless() which doesn't
   activate the app or make the panel key. With another app frontmost
   (the common case for a global-hotkey summon), the panel ordered to
   front but stayed BEHIND the active app's windows. User saw 'hotkey
   dismisses but won't bring forward' — actually summon was firing,
   the window just wasn't visible above the active app's chrome.

   Fix: NSApp.activate(ignoringOtherApps: true) + makeKeyAndOrderFront(nil).
   Also tightened isSummoned to require BOTH .isVisible AND .isKeyWindow,
   so the toggle correctly distinguishes 'visible-but-occluded' (→ summon)
   from 'visible-and-frontmost' (→ dismiss).

2. Settings… menu item was hardcoded .isEnabled = false with 'Coming
   soon' tooltip. Now wired to the wizard (firstLaunch: false) so the
   user can change API key, re-grant permissions, or rebind the hotkey
   from one place — those are the canonical settings until a dedicated
   panel ships.
2026-05-02 08:04:37 -05:00
james.maes 8fbe2d6d65 fix(menu): keep ClosureTarget alive for menu item lifetime via associated object
NSMenuItem.target is declared 'weak'. The previous code:
    setup.target = ClosureTarget(action: setupAction)
    setup.action = #selector(ClosureTarget.run)
created a ClosureTarget, assigned it to a weak property, and let it
deallocate immediately because nothing else retained it. By the time
the user clicked the menu item, target was nil and AppKit silently
dropped the click. All three closure-driven items broke this way:
'Setup…', 'Show Dev Overlay', 'Copy State Dump'.

Compiler had been telling us the whole time — three 'weak reference
will always be nil because the referenced object is deallocated here'
warnings on lines 16/28/33. The doc comment on the trampoline class
even claimed 'Strong-ref on the menu item is deliberate', but the
weak nature of NSMenuItem.target meant no strong ref existed.

Fix: factor menu-item construction into makeItem(title:action:),
which keeps the ClosureTarget alive for the menu item's lifetime
via objc_setAssociatedObject (.OBJC_ASSOCIATION_RETAIN_NONATOMIC).
Address-of-static used as the key per ObjC associated-object idiom.

Verified: build clean (3 weak-ref warnings gone, 0 new); manual
relaunch of Jarvis.app shows 'Setup…' opens the OnboardingWizard.
2026-05-02 07:10:37 -05:00
james.maes f8da4b7c39 feat(01-03): add MenuBarIconController + MenuBarContextMenu + JarvisAppTests target
- MenuBarIconController (@MainActor) owns NSStatusItem, loads Icon-MenuBar-Template asset with isTemplate=true, re-anchors layer.frame after anchorPoint change (Q7 gotcha #2), transitions across 5 HudStates via CAAnimationFactory with 150ms crossfade + rate-limited VoiceOver announcements (3s limiter per UI-SPEC Surface 3 line 421)
- MenuBarContextMenu builds Setup… / Settings… (disabled) / Show Dev Overlay / Copy State Dump / Quit Jarvis (⌘Q), with a ClosureTarget trampoline for closure-based actions
- Left-click vs right-click disambiguation via NSApp.currentEvent.type inspection (D-16 / UI-SPEC Surface 3 lines 406-409)
- JarvisAppTests Xcode unit-test bundle target added via project.yml; scheme wires test action against JarvisAppTests
- MenuBarIconControllerTests covers state transitions, no-op-on-same-state, context-menu items, and HudState voiceOverLabels (4 tests pass)
- PBXCopyFilesBuildPhase for Contents/Helpers re-applied post-xcodegen (Plan 01 invariant — MCP-05 acceptance-criteria grep)
2026-04-22 13:14:09 -05:00