Slice 2 of Round 4. With Slice 1 the probe layer now reports severity;
this slice routes severity to the banner system so the user CANNOT
silently dismiss a "your memory is dead, Jarvis will lie to you" warning.
- `BannerContent.nonDismissible: Bool` (default false).
- `HUDBanner` hides the dismiss X when `content.nonDismissible == true`.
- `HUDBannerCoordinator.dismissCurrent()` no-ops on non-dismissible
banners; the banner stays on screen until programmatically cleared
(`clear()` on app quit, or a re-probe finding the subsystem healthy
enqueues a different banner that pre-empts it).
- Two new presets:
* `bootHealthCritical(subsystem:reason:)` — priority 1, non-dismissible.
Title format: "Memory: critical failure". Body promises that the
agent will be warned not to rely on this subsystem (Slice 3).
* `bootHealthLoud(subsystem:reason:)` — priority 3, dismissible.
- Existing `bootHealthFailed(...)` preset kept as backwards-compat
fallback (no current call sites after this slice).
- `AppDelegate.enqueueFailedBootHealthBanners` rewritten to iterate
every non-ok subsystem and route by severity:
.critical → bootHealthCritical + `[system] CRITICAL` log line
.loud → bootHealthLoud
.soft → no banner (Status panel only)
Previously only `.failed` cases enqueued banners, which silently
dropped `.degraded` (the Toby case) entirely.
Two new tests in HUDBannerCoordinatorTests cover the new behavior:
* test_nonDismissibleBannerIgnoresDismiss
* test_criticalBannerPreemptsLowerPriorityCurrent
(Note: `xcodebuild test -only-testing JarvisAppTests` is pre-existing-
broken on this checkout due to onnxruntime static-link duplication —
confirmed by `git stash` before this slice. The test code compiles
cleanly via the regular App build; SPM-target tests for the
AgentCore-side severity model are green.)
Boundary gates 18/18 green. `swift test --package-path packages/AgentCore
--filter BootHealthOrchestratorTests` → 13/13.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
91 lines
3.8 KiB
Swift
91 lines
3.8 KiB
Swift
import SwiftUI
|
|
import AppKit
|
|
|
|
/// SwiftUI body for a single HUD banner. Layout per UI-SPEC Surface 5
|
|
/// "Internal layout (horizontal)" lines 467-469. The leading 4pt accent stripe
|
|
/// uses `BrandColor.arcReactorGlow` which already handles Increase Contrast
|
|
/// fallback and light/dark variants.
|
|
struct HUDBanner: View {
|
|
let content: BannerContent
|
|
let onAction: () -> Void
|
|
let onDismiss: () -> Void
|
|
|
|
var body: some View {
|
|
HStack(spacing: 0) {
|
|
Rectangle()
|
|
.fill(Color(nsColor: BrandColor.arcReactorGlow))
|
|
.frame(width: 4)
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
HStack(alignment: .top) {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(content.title)
|
|
.font(.headline)
|
|
.foregroundColor(Color(NSColor.labelColor))
|
|
// `.fixedSize(horizontal: false, vertical: true)` opts
|
|
// into multi-line wrap instead of tail-truncation in
|
|
// the 360pt-wide banner. `HUDBannerPanel.positionTopTrailing`
|
|
// re-reads `contentView.fittingSize.height` on host, so
|
|
// wrapping picks up extra rows in the panel sizing.
|
|
Text(content.body)
|
|
.font(.body)
|
|
.foregroundColor(Color(NSColor.secondaryLabelColor))
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
Spacer(minLength: 8)
|
|
// Round 4 — hide the dismiss X for non-dismissible
|
|
// banners (critical subsystem failures). The coordinator
|
|
// also no-ops `dismissCurrent` for these, but hiding
|
|
// the affordance prevents user confusion.
|
|
if !content.nonDismissible {
|
|
Button(action: onDismiss) {
|
|
Image(systemName: "xmark")
|
|
.font(.system(size: 11, weight: .semibold))
|
|
.frame(width: 24, height: 24)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.foregroundColor(Color(NSColor.secondaryLabelColor))
|
|
.accessibilityLabel("Dismiss notification")
|
|
}
|
|
}
|
|
if let action = content.action {
|
|
Button(action.label, action: onAction)
|
|
.buttonStyle(.borderedProminent)
|
|
.controlSize(.small)
|
|
}
|
|
}
|
|
.padding(.leading, 16)
|
|
.padding(.trailing, 16)
|
|
.padding(.vertical, 12)
|
|
}
|
|
.background(
|
|
VisualEffectBackground(material: .hudWindow, blendingMode: .behindWindow)
|
|
)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.strokeBorder(Color(NSColor.separatorColor), lineWidth: 1)
|
|
)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
}
|
|
|
|
/// Bridge `NSVisualEffectView` into SwiftUI for the `.hudWindow` material behind
|
|
/// banner content.
|
|
struct VisualEffectBackground: NSViewRepresentable {
|
|
let material: NSVisualEffectView.Material
|
|
let blendingMode: NSVisualEffectView.BlendingMode
|
|
|
|
func makeNSView(context: Context) -> NSVisualEffectView {
|
|
let v = NSVisualEffectView()
|
|
v.material = material
|
|
v.blendingMode = blendingMode
|
|
v.state = .active
|
|
return v
|
|
}
|
|
|
|
func updateNSView(_ nsView: NSVisualEffectView, context: Context) {
|
|
nsView.material = material
|
|
nsView.blendingMode = blendingMode
|
|
}
|
|
}
|