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>
This commit is contained in:
james.maesandClaude Opus 4.7 committed 2026-05-11 14:12:37 -05:00
1 parent 687b80b3f0
commit 67748f5336
4 files changed
+304 -2

No files matched your search

+24 -1
View File
@@ -183,6 +183,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
/// are user-driven from the Status menu.
private var bootHealthTask: Task<Void, Never>?
/// Round 3 — lazy `StatusPanel`. Constructed on first menu click;
/// kept alive across closes via `isReleasedWhenClosed = false`.
private var statusPanel: StatusPanel?
/// Drain task spawned in `applicationWillFinishLaunching` — reads
/// envelopes from `orchToReplayChannel` and forwards to `replayLog`.
/// CR-02: production consumer for ME-04. Previously this task
@@ -748,6 +752,24 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
enqueueFailedBootHealthBanners(snapshot: snapshot)
}
/// Round 3 — opens the Status panel (lazy-creates on first click).
/// The panel's `.task { await model.reprobe() }` SwiftUI modifier
/// fires a fresh probe sweep on every appearance, so the user always
/// sees live state — never a stale boot snapshot.
@MainActor
func openStatusPanel() {
if statusPanel == nil {
let model = StatusPanelModel(
orchestrator: bootHealthOrchestrator,
probeRunner: { [weak self] in
await self?.runBootHealth()
}
)
statusPanel = StatusPanel(model: model)
}
statusPanel?.present()
}
/// Constructs one probe per subsystem and registers it on the
/// orchestrator. Each probe captures a strong reference to the live
/// actor it queries — at probe-time the actor must be addressable.
@@ -2298,7 +2320,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
setupAction: { [weak self] in self?.openWizard(firstLaunch: false) },
settingsAction: { [weak self] in self?.openSettings() },
devOverlayToggleAction: { [weak self] in self?.toggleDevOverlay() },
stateDumpAction: { [weak self] in self?.copyStateDump() }
stateDumpAction: { [weak self] in self?.copyStateDump() },
statusAction: { [weak self] in self?.openStatusPanel() }
)
let controller = MenuBarIconController(statusItem: item, contextMenu: menu)
controller.setLeftClickAction { [weak self] in self?.toggleHUD() }
+5 -1
View File
@@ -8,7 +8,8 @@ public enum MenuBarContextMenu {
setupAction: @escaping () -> Void,
settingsAction: @escaping () -> Void,
devOverlayToggleAction: @escaping () -> Void,
stateDumpAction: @escaping () -> Void
stateDumpAction: @escaping () -> Void,
statusAction: @escaping () -> Void
) -> NSMenu {
let menu = NSMenu()
@@ -22,6 +23,9 @@ public enum MenuBarContextMenu {
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))
+271
View File
@@ -0,0 +1,271 @@
import AppKit
import SwiftUI
import AgentCore
// Round 3 — Status menu dialog per the 2026-05-11 handoff.
//
// `NSPanel` + SwiftUI body that renders the current `BootHealthSnapshot`,
// re-probes on every open ("NOT FAKED 0 100% real only" — never show a
// stale cached snapshot), and exposes "Re-probe all" + "Copy report"
// buttons. AppKit, not WKWebView: this is an operator/dev tool, not a
// user-facing surface, and shouldn't ride the R3F render pipeline.
//
// Non-modal — uses `makeKeyAndOrderFront(_:)`, not `.runModal()`. Project
// rule `scripts/check-no-modal-presentation.sh` is the gate that keeps it
// that way.
// MARK: - StatusPanelModel
/// View-model wrapping the `BootHealthOrchestrator`. Exposes a published
/// `snapshot` for SwiftUI, an `isProbing` flag for the spinner, and a
/// `reprobe()` async method that AppDelegate's `runBootHealth()` drives
/// (so the snapshot read in the panel matches the snapshot logged to
/// the system log — single source of truth).
@MainActor
public final class StatusPanelModel: ObservableObject {
@Published public var snapshot: BootHealthSnapshot?
@Published public var isProbing: Bool = false
@Published public var copyConfirmation: String?
private let orchestrator: BootHealthOrchestrator
/// Closure that runs the same registration + logging + banner-enqueue
/// sweep AppDelegate ran at boot. Injected so the panel's "Re-probe"
/// button hits the production code path, not a parallel one.
private let probeRunner: @MainActor () async -> Void
public init(
orchestrator: BootHealthOrchestrator,
probeRunner: @escaping @MainActor () async -> Void
) {
self.orchestrator = orchestrator
self.probeRunner = probeRunner
}
/// Run a fresh sweep, then read the new snapshot off the orchestrator.
public func reprobe() async {
isProbing = true
copyConfirmation = nil
await probeRunner()
snapshot = await orchestrator.lastSnapshot()
isProbing = false
}
/// Encode the snapshot as pretty-printed JSON and write to the
/// general pasteboard. Updates `copyConfirmation` for the caption
/// next to the button — auto-clears on next re-probe.
public func copyReport() {
guard let snap = snapshot else { return }
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
encoder.dateEncodingStrategy = .iso8601
guard let data = try? encoder.encode(snap),
let s = String(data: data, encoding: .utf8) else {
copyConfirmation = "Encode failed"
return
}
let pb = NSPasteboard.general
pb.clearContents()
pb.setString(s, forType: .string)
copyConfirmation = "Copied \(data.count) B"
}
}
// MARK: - SwiftUI view
struct StatusPanelView: View {
@ObservedObject var model: StatusPanelModel
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Jarvis Status").font(.title2).bold()
Spacer()
if model.isProbing {
ProgressView().controlSize(.small)
}
}
Divider()
ScrollView {
VStack(alignment: .leading, spacing: 6) {
if let snap = model.snapshot {
ForEach(snap.subsystems, id: \.name) { health in
SubsystemRow(health: health)
}
Text("Probed \(snap.subsystems.count) subsystems at \(snap.producedAt.formatted(date: .omitted, time: .standard))")
.font(.caption)
.foregroundStyle(.secondary)
.padding(.top, 6)
} else if model.isProbing {
Text("Probing…")
.foregroundStyle(.secondary)
} else {
Text("Probes haven't run yet.")
.foregroundStyle(.secondary)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(maxHeight: .infinity)
Divider()
HStack(spacing: 12) {
Button("Re-probe all") {
Task { await model.reprobe() }
}
.disabled(model.isProbing)
.keyboardShortcut("r", modifiers: [.command])
Spacer()
if let conf = model.copyConfirmation {
Text(conf)
.font(.caption)
.foregroundStyle(.secondary)
.transition(.opacity)
}
Button("Copy report") {
model.copyReport()
}
.disabled(model.snapshot == nil)
.keyboardShortcut("c", modifiers: [.command, .shift])
}
}
.padding()
.frame(minWidth: 480, minHeight: 380)
.task {
// Always re-probe on open. The handoff's mandate is live
// state, never a cached boot snapshot.
await model.reprobe()
}
}
}
// MARK: - Row
private struct SubsystemRow: View {
let health: SubsystemHealth
var body: some View {
HStack(alignment: .top, spacing: 10) {
Text(glyph)
.font(.body.bold())
.foregroundColor(color)
.frame(width: 18, alignment: .center)
.accessibilityLabel(statusLabel)
VStack(alignment: .leading, spacing: 2) {
HStack {
Text(health.name)
.font(.body.weight(.medium))
Text(statusLabel)
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
Text("\(health.latencyMs) ms")
.font(.caption.monospaced())
.foregroundStyle(.secondary)
}
Text(health.evidence)
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
if let reason = reasonText {
Text(reason)
.font(.caption)
.foregroundColor(reasonColor)
.fixedSize(horizontal: false, vertical: true)
}
}
}
.padding(.vertical, 4)
}
private var glyph: String {
switch health.status {
case .ok: return "✓"
case .degraded: return "⚠"
case .failed: return "✗"
case .unknown: return "?"
}
}
private var color: Color {
switch health.status {
case .ok: return .green
case .degraded: return .orange
case .failed: return .red
case .unknown: return .gray
}
}
private var statusLabel: String {
switch health.status {
case .ok: return "ok"
case .degraded: return "degraded"
case .failed: return "failed"
case .unknown: return "unknown"
}
}
/// The reason string is only meaningful for non-ok statuses; for `.ok`
/// the evidence line is already the concrete proof of health.
private var reasonText: String? {
switch health.status {
case .ok: return nil
case .degraded(let r), .failed(let r), .unknown(let r): return r
}
}
private var reasonColor: Color {
switch health.status {
case .failed: return .red
case .degraded: return .orange
default: return .secondary
}
}
}
// MARK: - NSPanel
/// Floating panel that owns the SwiftUI hosting view. Constructed once
/// on first `Status…` menu click and kept alive across closes via
/// `isReleasedWhenClosed = false` (default NSPanel behavior releases the
/// window on close → second open crashes).
@MainActor
public final class StatusPanel: NSPanel {
public let model: StatusPanelModel
private let hosting: NSHostingView<StatusPanelView>
public init(model: StatusPanelModel) {
self.model = model
self.hosting = NSHostingView(rootView: StatusPanelView(model: model))
super.init(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 380),
styleMask: [.titled, .closable, .resizable, .nonactivatingPanel],
backing: .buffered,
defer: false
)
title = "Jarvis Status"
isFloatingPanel = false
becomesKeyOnlyIfNeeded = false
isReleasedWhenClosed = false
hidesOnDeactivate = false
hosting.autoresizingMask = [.width, .height]
contentView = hosting
}
/// Bring panel onto screen + give it focus. Non-modal — caller
/// continues running. SwiftUI's `.task` modifier on the view body
/// fires a re-probe automatically on the appearance pass.
public func present() {
if !isVisible {
center()
}
makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
}
}
+4
View File
@@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
01787A3F29649644D0B3090D /* MCPAppleScriptMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5077B9E3E7541D72CE0EB1B /* MCPAppleScriptMain.swift */; };
B0010002000000000000B002 /* BootHealthProbes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0010001000000000000B001 /* BootHealthProbes.swift */; };
B0020002000000000000B002 /* StatusPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0020001000000000000B001 /* StatusPanel.swift */; };
0288ABA48E6EC8450BE4BC5F /* BannerContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BF0892A5DFF2AD38A93834 /* BannerContent.swift */; };
07E7F32B3857ED215DE59FBE /* Keychain in Frameworks */ = {isa = PBXBuildFile; productRef = 1440CCDB2CC412B099BD0310 /* Keychain */; };
0B1E24FF2DB306F28A36680E /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 8B5C832427B631F1F6EED6C2 /* Logging */; };
@@ -146,6 +147,7 @@
/* Begin PBXFileReference section */
00C01D284ECCFC986C7F2E16 /* Keychain */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Keychain; path = packages/Keychain; sourceTree = SOURCE_ROOT; };
B0010001000000000000B001 /* BootHealthProbes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BootHealthProbes.swift; sourceTree = "<group>"; };
B0020001000000000000B001 /* StatusPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusPanel.swift; sourceTree = "<group>"; };
024EE01ED6CC98A61A5CD96C /* WizardStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardStateTests.swift; sourceTree = "<group>"; };
0358E0CAC1377752398FA3C4 /* ReplayingToolResultObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplayingToolResultObserver.swift; sourceTree = "<group>"; };
096AD64E7DB48B0F1EE47F89 /* HUDBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HUDBanner.swift; sourceTree = "<group>"; };
@@ -370,6 +372,7 @@
604976A6CA41878D67AF7B71 /* CAAnimationFactory.swift */,
B5540C076C8C87A8060B8881 /* MenuBarContextMenu.swift */,
4DFECDF801143C9F672B5FB7 /* MenuBarIconController.swift */,
B0020001000000000000B001 /* StatusPanel.swift */,
);
path = MenuBar;
sourceTree = "<group>";
@@ -1344,6 +1347,7 @@
8EF7109838AD91E95506BD1B /* MCPRuntimeWiring.swift in Sources */,
A20E24D0AB73FFBBE4AB9B9B /* MenuBarContextMenu.swift in Sources */,
9933987292A1035BB048D0B9 /* MenuBarIconController.swift in Sources */,
B0020002000000000000B002 /* StatusPanel.swift in Sources */,
7EFA9CFF57B04C03347D3174 /* OnboardingWizardController.swift in Sources */,
EF9A869A3403AF044999784F /* ProductionChunkPump.swift in Sources */,
63C78894699F60D1DE3A6715 /* RejectReasonCopy.swift in Sources */,