Commit Graph
11 Commits
Author SHA1 Message Date
james.maesandClaude Opus 4.7 a9531e65e3 feat(round-4): menu-bar icon turns red when subsystems are degraded
Slice 4 of Round 4 — the visible polish. With Slices 1–3 the agent
preamble + non-dismissible banner cover the "agent must know +
user must see" loop, but a closed HUD with no banner visible can
still hide the warning. This slice paints the menu-bar icon red so
the warning is glanceable from anywhere on the screen.

Option A from the slice plan (single-writer-safe):

- `MenuBarIconController.applyHealth(_ health: OverallHealth)` paints
  `button.contentTintColor = NSColor.systemRed` for `.loud` and
  `.critical`, clears the tint for `.ok` and `.soft`. Template images
  recolor automatically with `contentTintColor` so no second icon
  asset is needed.
- `currentHealth: OverallHealth?` exposed read-only for tests.
- `AppDelegate.runBootHealth` calls `menuBarController?.applyHealth(
  snapshot.overallHealth)` after computing the degradation summary,
  so the icon updates on every probe sweep (cold boot + Status panel
  re-probe).

Single-writer invariant for HudState (HUD-08) is intact:
`contentTintColor` lives on `NSStatusItemButton`, not in HudState.
`scripts/check-single-writer-hudstate.sh` is green.

1 new test in MenuBarIconControllerTests:
  * test_applyHealthTintsRedForLoudAndCritical — exercises all four
    OverallHealth cases and asserts contentTintColor toggles correctly.

Boundary gates 18/18 green; App target compiles cleanly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 15:59:48 -05:00
james.maesandClaude Opus 4.7 cfd469f72b feat(round-4): probe severity model — ok/soft/loud/critical
Round 4 of boot-health work. The Toby-the-dog incident — Jarvis said "Got
it" while the embedder model was missing and the fact never persisted —
proved that Round 2's probe banner was too quiet. Probes were already
reporting `.degraded` correctly; what was missing was a way for the App
layer to route a critical failure differently from a soft one (e.g.,
non-dismissible banner for memory off vs. silent Status row for camera
TCC not yet requested).

This slice introduces a severity axis without changing behavior:

- `FailureSeverity { critical, loud, soft }` on `ProbeStatus`'s non-ok
  cases (degraded/failed/unknown). `.ok` carries no severity.
- `ProbeStatus.severity` accessor (nil for `.ok`).
- `BootHealthSnapshot.overallHealth: OverallHealth { ok, soft, loud,
  critical }` — worst severity wins; `.ok` only if every subsystem ok.
- Each concrete probe annotates its failure modes:
  * Memory: failed → .critical (agent grounding; the Toby case)
  * Anthropic: missing/empty key → .critical (agent can't run)
  * Ollama: missing models → .loud (fact extraction broken, chat fine)
  * Voice: mic denied → .loud; dormant install → .soft
  * Vision: TCC denied → .soft (vision is optional)
  * MCP: 0 tools → .critical; <minimum → .loud
  * Replay: init failed → .critical; missing file → .loud
  * Webview: mismatched/timedOut → .critical; sentHello → .soft
- DormantSubsystemProbe gains `severity` parameter (default .soft);
  AppDelegate wires memory-dormant as .critical.

No behavioral change yet — banners still treat all failures equally;
the agent system prompt is unchanged. Slices 2 + 3 land those.

Tests: 13/13 in BootHealthOrchestratorTests pass, including 4 new tests
for overallHealth + severity accessor + JSON roundtrip with severity.
2 pre-existing failures in LLMEventTests/OllamaRequestBodyTests are
unrelated (qwen model-ID literal drift from commit 8d0a959, present
before this slice — confirmed via git stash).

Boundary gates 18/18 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 15:24:43 -05:00
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 da42c079fd feat(06-05): AppDelegate production wiring + VoiceWiringTests (W1-W4)
- AppDelegate: import Voice, add voiceController/pushToTalk/muteWakeWord properties
- AppDelegate: installVoice() async method wires VoiceController to dormantVoiceContinuation
- AppDelegate: NullOrchestratorAdapter/NullTTSAdapter/NullBusEmitterAdapter (Phase 7 stubs)
- AppDelegate: AppDelegateBannerAdapter bridges VoiceBannerInterface→HUDBannerCoordinator
- MenuBarIconController: contextMenu is now public (for MuteWakeWord)
- VoiceWiringTests: W1-W4 type-level wiring verification
- NOTE: check-app-builds.sh fails on pnpm (pre-existing infra limit); Swift compiles clean
2026-04-27 13:56:34 -05:00
james.maesandClaude Opus 4.7 d7a192a446 fix(menu-bar): use popUpMenu instead of toggle-menu pattern
The previous click handler tried to render the right-click context menu
via:

    statusItem.menu = contextMenu
    sender.performClick(nil)
    statusItem.menu = nil

This is racy. NSStatusItem can latch the menu reference on a separate
runloop pass after we set it, so the post-click `nil` assignment doesn't
always take effect. Once the menu is latched, NSStatusItem bypasses the
button's action selector entirely — every left- AND right-click then
opens the context menu, and the HUD never gets summoned because
`onLeftClick?()` no longer fires.

User-observed symptom: "when I clicked on it (left or right mouse) it
showed the right hand menu no matter what" after the first right-click
of the session.

Fix: call `statusItem.popUpMenu(contextMenu)` directly. The menu shows
for this click only — no persistent assignment, no race. `popUpMenu`
was deprecated in macOS 10.14 with Apple recommending "use the menu
property instead," but that recommendation is exactly what produces
this bug. The function still works through Tahoe (verified
empirically). When/if Apple removes it, replacement is a manual
`NSMenu.popUp(positioning:at:in:)` against the status-item button.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 16:26:22 -05:00
james.maes ca17844883 feat(03-01): extend HudState enum 5→7 cases with voiceOverLabels
- Append .reconfiguring and .booting to App/Theme/HudState.swift;
  preserve Phase 1 declaration order + existing labels verbatim.
- Add voiceOverLabel arms 'Jarvis, reconfiguring audio' / 'Jarvis,
  starting up' per RESEARCH §HudState Enum Wire Format.
- MenuBarIconController: add forced exhaustive switch arm for the
  two new cases; both share .idle profile (no animation) with
  TODO for Phase 3/4 dedicated treatment per HUD-08 precedence.
- HudStateEnumTests: 6 methods covering case count, non-empty +
  unique voiceOverLabels, spec-verbatim labels, exhaustive switch.
2026-04-23 18:47:46 -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
james.maes c19d6b2cad feat(01-03): add HudState, BrandColors, CAAnimationFactory, menu-bar template PDF
- HudState enum (5 cases) with verbatim UI-SPEC Surface 3 VoiceOver labels
- BrandColor.arcReactorGlow: #1E88E5@85% light / #64B5F6@90% dark with Increase Contrast fallback to controlAccentColor
- CAAnimationFactory with makeBreath/makeRotate/makeShimmer/makeGlow/makeStateCrossfade, each Reduce Motion-guarded
- 22×22pt arc-reactor silhouette placeholder PDF (outer 18pt ring + mid 12pt ring + filled 4pt core + six 60° spokes) via Core Graphics
- Icon-MenuBar-Template.imageset Contents.json pointed at the PDF with template-rendering-intent=template and preserves-vector-representation=true
2026-04-22 12:50:42 -05:00