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>
84 lines
3.1 KiB
Swift
84 lines
3.1 KiB
Swift
import AppKit
|
|
import ObjectiveC.runtime
|
|
|
|
/// Builds the menu-bar right-click / Ctrl-click menu per UI-SPEC Surface 4 lines 428-436.
|
|
@MainActor
|
|
public enum MenuBarContextMenu {
|
|
public static func build(
|
|
setupAction: @escaping () -> Void,
|
|
settingsAction: @escaping () -> Void,
|
|
devOverlayToggleAction: @escaping () -> Void,
|
|
stateDumpAction: @escaping () -> Void,
|
|
statusAction: @escaping () -> Void
|
|
) -> NSMenu {
|
|
let menu = NSMenu()
|
|
|
|
menu.addItem(makeItem(title: "Setup…", action: setupAction))
|
|
// Settings… opens the freeform settings panel
|
|
// (`SettingsWindowController`). The user can change the API key,
|
|
// re-grant Input Monitoring, or rebind the global hotkey from there.
|
|
// Distinct from Setup… (the sequential first-run wizard); both
|
|
// surfaces share the same underlying `WizardState`.
|
|
menu.addItem(makeItem(title: "Settings…", action: settingsAction))
|
|
|
|
menu.addItem(NSMenuItem.separator())
|
|
|
|
// Round 3 — operator-facing live subsystem health. Opens an
|
|
// AppKit panel that re-probes on every appearance.
|
|
menu.addItem(makeItem(title: "Status…", action: statusAction))
|
|
menu.addItem(makeItem(title: "Show Dev Overlay", action: devOverlayToggleAction))
|
|
menu.addItem(makeItem(title: "Copy State Dump", action: stateDumpAction))
|
|
|
|
menu.addItem(NSMenuItem.separator())
|
|
|
|
let quit = NSMenuItem(
|
|
title: "Quit Jarvis",
|
|
action: #selector(NSApplication.terminate(_:)),
|
|
keyEquivalent: "q"
|
|
)
|
|
quit.keyEquivalentModifierMask = [.command]
|
|
menu.addItem(quit)
|
|
|
|
return menu
|
|
}
|
|
|
|
/// Build an NSMenuItem with a closure action.
|
|
///
|
|
/// `NSMenuItem.target` is a `weak` property, so the obvious
|
|
/// `item.target = ClosureTarget(...)` pattern lets the trampoline
|
|
/// deallocate immediately and the menu item silently does nothing
|
|
/// when clicked. We keep the trampoline alive for the menu item's
|
|
/// lifetime via objc_setAssociatedObject on the item itself.
|
|
private static func makeItem(title: String, action: @escaping () -> Void) -> NSMenuItem {
|
|
let item = NSMenuItem(title: title, action: nil, keyEquivalent: "")
|
|
let target = ClosureTarget(action: action)
|
|
item.target = target
|
|
item.action = #selector(ClosureTarget.run)
|
|
objc_setAssociatedObject(
|
|
item,
|
|
&MenuBarContextMenu.trampolineKey,
|
|
target,
|
|
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
|
|
)
|
|
return item
|
|
}
|
|
|
|
/// Address used as the associated-object key. The address itself is the
|
|
/// key — the value of `trampolineKey` is irrelevant.
|
|
private static var trampolineKey: UInt8 = 0
|
|
}
|
|
|
|
/// Objective-C-visible trampoline so `NSMenuItem` can call closure-based actions.
|
|
/// Strong-ref is established by `objc_setAssociatedObject` in `makeItem`.
|
|
@MainActor
|
|
private final class ClosureTarget: NSObject {
|
|
let action: () -> Void
|
|
init(action: @escaping () -> Void) {
|
|
self.action = action
|
|
super.init()
|
|
}
|
|
@objc func run() {
|
|
action()
|
|
}
|
|
}
|