From 7272928e1faede4f64911be827283979e200383a Mon Sep 17 00:00:00 2001 From: hhh2210 Date: Thu, 4 Jun 2026 14:12:11 +0800 Subject: [PATCH 1/5] Reduce icon observation churn --- .../StatusItemController+Animation.swift | 48 +++--- ...StatusItemController+IconObservation.swift | 70 +++++++++ Sources/CodexBar/StatusItemController.swift | 45 ------ ...tusItemIconObservationSignatureTests.swift | 146 +++++++++++++++++- 4 files changed, 234 insertions(+), 75 deletions(-) create mode 100644 Sources/CodexBar/StatusItemController+IconObservation.swift diff --git a/Sources/CodexBar/StatusItemController+Animation.swift b/Sources/CodexBar/StatusItemController+Animation.swift index a2181b83c1..853098d4ff 100644 --- a/Sources/CodexBar/StatusItemController+Animation.swift +++ b/Sources/CodexBar/StatusItemController+Animation.swift @@ -232,7 +232,7 @@ extension StatusItemController { } @discardableResult - func applyIcon(phase: Double?) -> Bool { // swiftlint:disable:this function_body_length + func applyIcon(phase: Double?) -> Bool { guard let button = self.statusItem.button else { return false } let style = self.store.iconStyle @@ -269,17 +269,7 @@ extension StatusItemController { // In show-used mode, `0` means "unused", not "missing". Keep the weekly lane present. weekly = Self.loadingPercentEpsilon } - let codexProjection = self.store.codexConsumerProjectionIfNeeded( - for: primaryProvider, - surface: .menuBar, - snapshotOverride: snapshot, - now: snapshot?.updatedAt ?? Date()) - var credits: Double? = - codexProjection?.menuBarFallback == .creditsBalance - ? self.store.codexMenuBarCreditsRemaining( - snapshotOverride: snapshot, - now: snapshot?.updatedAt ?? Date()) - : nil + var credits = self.menuBarCreditsRemainingForIcon(provider: primaryProvider, snapshot: snapshot) var stale = self.store.isStale(provider: primaryProvider) var morphProgress: Double? @@ -486,17 +476,7 @@ extension StatusItemController { // In show-used mode, `0` means "unused", not "missing". Keep the weekly lane present. weekly = Self.loadingPercentEpsilon } - let codexProjection = self.store.codexConsumerProjectionIfNeeded( - for: provider, - surface: .menuBar, - snapshotOverride: snapshot, - now: snapshot?.updatedAt ?? Date()) - var credits: Double? = - codexProjection?.menuBarFallback == .creditsBalance - ? self.store.codexMenuBarCreditsRemaining( - snapshotOverride: snapshot, - now: snapshot?.updatedAt ?? Date()) - : nil + var credits = self.menuBarCreditsRemainingForIcon(provider: provider, snapshot: snapshot) var stale = self.store.isStale(provider: provider) var morphProgress: Double? @@ -587,11 +567,27 @@ extension StatusItemController { return false } - private static func iconSignatureValue(_ value: Double?) -> String { + static func iconSignatureValue(_ value: Double?) -> String { guard let value else { return "nil" } return String(format: "%.3f", value) } + func menuBarCreditsRemainingForIcon(provider: UsageProvider, snapshot: UsageSnapshot?) -> Double? { + guard provider == .codex, + let creditsRemaining = self.store.credits?.remaining, + creditsRemaining > 0 + else { + return nil + } + + let rateWindows = [snapshot?.primary, snapshot?.secondary].compactMap(\.self) + guard rateWindows.isEmpty || rateWindows.contains(where: { $0.remainingPercent <= 0 }) + else { + return nil + } + return creditsRemaining + } + func quotaWarningFlashActive(provider: UsageProvider, now: Date = Date()) -> Bool { guard let until = self.quotaWarningFlashUntil[provider] else { return false } if until > now { return true } @@ -894,7 +890,7 @@ extension StatusItemController { self.menuBarMetricWindow(for: provider, snapshot: snapshot) } - private func primaryProviderForUnifiedIcon() -> UsageProvider { + func primaryProviderForUnifiedIcon() -> UsageProvider { // When "show highest usage" is enabled, auto-select the provider closest to rate limit. if self.settings.menuBarShowsHighestUsage, self.shouldMergeIcons, @@ -966,7 +962,7 @@ extension StatusItemController { self.tickBlink(now: now) } - private func shouldAnimate(provider: UsageProvider, mergeIcons: Bool? = nil) -> Bool { + func shouldAnimate(provider: UsageProvider, mergeIcons: Bool? = nil) -> Bool { if self.store.debugForceAnimation { return true } let isMerged = mergeIcons ?? self.shouldMergeIcons diff --git a/Sources/CodexBar/StatusItemController+IconObservation.swift b/Sources/CodexBar/StatusItemController+IconObservation.swift new file mode 100644 index 0000000000..bbeb952ab7 --- /dev/null +++ b/Sources/CodexBar/StatusItemController+IconObservation.swift @@ -0,0 +1,70 @@ +import CodexBarCore +import Foundation + +extension StatusItemController { + func storeIconObservationSignature() -> String { + let showBrandPercent = self.settings.menuBarShowsBrandIconWithPercent + let mergeIcons = self.shouldMergeIcons + let visibleProviders = self.store.enabledProvidersForDisplay().map(\.rawValue).sorted().joined(separator: ",") + let providerSignatures: String + let primaryProvider: UsageProvider? + if mergeIcons { + let primary = self.primaryProviderForUnifiedIcon() + primaryProvider = primary + providerSignatures = [ + self.providerStoreIconObservationSignature(for: primary, showBrandPercent: showBrandPercent), + "mergedStatus=\(self.mergedIconStatusIndicator().rawValue)", + ].joined(separator: "||") + } else { + primaryProvider = nil + providerSignatures = UsageProvider.allCases + .filter { self.isVisible($0) } + .map { self.providerStoreIconObservationSignature(for: $0, showBrandPercent: showBrandPercent) } + .joined(separator: "||") + } + return [ + "merge=\(mergeIcons ? "1" : "0")", + "visible=\(visibleProviders)", + "primary=\(primaryProvider?.rawValue ?? "nil")", + "iconStyle=\(String(describing: self.store.iconStyle))", + "showUsed=\(self.settings.usageBarsShowUsed ? "1" : "0")", + "brandPercent=\(showBrandPercent ? "1" : "0")", + "needsAnimation=\(self.needsMenuBarIconAnimation() ? "1" : "0")", + providerSignatures, + ].joined(separator: "|") + } + + private func providerStoreIconObservationSignature(for provider: UsageProvider, showBrandPercent: Bool) -> String { + let snapshot = self.store.snapshot(for: provider) + let style = self.store.style(for: provider) + let resolved = snapshot.map { + IconRemainingResolver.resolvedPercents( + snapshot: $0, + style: style, + showUsed: self.settings.usageBarsShowUsed) + } + let creditsRemaining = self.menuBarCreditsRemainingForIcon(provider: provider, snapshot: snapshot) + let displayText = showBrandPercent ? self.menuBarDisplayText(for: provider, snapshot: snapshot) : nil + + return [ + provider.rawValue, + "style=\(String(describing: style))", + "primary=\(Self.iconSignatureValue(resolved?.primary))", + "weekly=\(Self.iconSignatureValue(resolved?.secondary))", + "credits=\(Self.iconSignatureValue(creditsRemaining))", + "stale=\(self.store.isStale(provider: provider) ? "1" : "0")", + "status=\(self.store.statusIndicator(for: provider).rawValue)", + "anim=\(self.shouldAnimate(provider: provider) ? "1" : "0")", + "refreshing=\(self.store.refreshingProviders.contains(provider) ? "1" : "0")", + "text=\(displayText ?? "nil")", + ].joined(separator: "|") + } + + private func mergedIconStatusIndicator() -> ProviderStatusIndicator { + for provider in self.store.enabledProvidersForDisplay() { + let indicator = self.store.statusIndicator(for: provider) + if indicator.hasIssue { return indicator } + } + return .none + } +} diff --git a/Sources/CodexBar/StatusItemController.swift b/Sources/CodexBar/StatusItemController.swift index fa60ca95a1..747a3dd480 100644 --- a/Sources/CodexBar/StatusItemController.swift +++ b/Sources/CodexBar/StatusItemController.swift @@ -467,51 +467,6 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin } } - func storeIconObservationSignature() -> String { - let showBrandPercent = self.settings.menuBarShowsBrandIconWithPercent - let mergeIcons = self.shouldMergeIcons - let needsAnimation = self.needsMenuBarIconAnimation() - let providerSignatures = UsageProvider.allCases.map { - self.providerStoreIconObservationSignature(for: $0, showBrandPercent: showBrandPercent) - }.joined(separator: "||") - let visibleProviders = self.store.enabledProvidersForDisplay().map(\.rawValue).sorted().joined(separator: ",") - return [ - "merge=\(mergeIcons ? "1" : "0")", - "visible=\(visibleProviders)", - "iconStyle=\(String(describing: self.store.iconStyle))", - "brandPercent=\(showBrandPercent ? "1" : "0")", - "needsAnimation=\(needsAnimation ? "1" : "0")", - providerSignatures, - ].joined(separator: "|") - } - - private func providerStoreIconObservationSignature(for provider: UsageProvider, showBrandPercent: Bool) -> String { - let snapshot = self.store.snapshot(for: provider) - let stale = self.store.isStale(provider: provider) - let status = self.store.statusIndicator(for: provider).rawValue - let isVisibleForAnimation = self.shouldMergeIcons ? self.isEnabled(provider) : self.isVisible(provider) - let isAnimating = isVisibleForAnimation && !stale && snapshot == nil - let isRefreshingWarpPlaceholder = self.store.refreshingProviders.contains(provider) - let creditsRemaining = provider == .codex - ? self.store.codexMenuBarCreditsRemaining( - snapshotOverride: snapshot, - now: snapshot?.updatedAt ?? Date()) - : nil - let displayText = showBrandPercent ? self.menuBarDisplayText(for: provider, snapshot: snapshot) : nil - - return [ - provider.rawValue, - "style=\(String(describing: self.store.style(for: provider)))", - "snapshot=\(String(describing: snapshot))", - "stale=\(stale ? "1" : "0")", - "status=\(status)", - "anim=\(isAnimating ? "1" : "0")", - "refreshing=\(isRefreshingWarpPlaceholder ? "1" : "0")", - "credits=\(String(describing: creditsRemaining))", - "text=\(displayText ?? "nil")", - ].joined(separator: "|") - } - private func observeDebugForceAnimation() { withObservationTracking { _ = self.store.debugForceAnimation diff --git a/Tests/CodexBarTests/StatusItemIconObservationSignatureTests.swift b/Tests/CodexBarTests/StatusItemIconObservationSignatureTests.swift index 6660c264ee..5d2968a9e8 100644 --- a/Tests/CodexBarTests/StatusItemIconObservationSignatureTests.swift +++ b/Tests/CodexBarTests/StatusItemIconObservationSignatureTests.swift @@ -20,6 +20,9 @@ struct StatusItemIconObservationSignatureTests { if let codexMeta = registry.metadata[.codex] { settings.setProviderEnabled(provider: .codex, metadata: codexMeta, enabled: true) } + if let claudeMeta = registry.metadata[.claude] { + settings.setProviderEnabled(provider: .claude, metadata: claudeMeta, enabled: false) + } let fetcher = UsageFetcher() let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) @@ -55,6 +58,126 @@ struct StatusItemIconObservationSignatureTests { #expect(controller.storeIconObservationSignature() == baseline) } + @Test + func `store icon observation signature ignores non visual snapshot churn`() { + let (_, store, controller) = self.makeController( + suiteName: "StatusItemIconObservationSignatureTests-snapshot-metadata") + defer { controller.releaseStatusItemsForTesting() } + + let baseline = controller.storeIconObservationSignature() + + store._setSnapshotForTesting( + Self.makeSnapshot( + provider: .codex, + email: "rotated-account@example.com", + updatedAt: Date(timeIntervalSince1970: 200)), + provider: .codex) + + let signature = controller.storeIconObservationSignature() + + #expect(signature == baseline) + #expect(!signature.contains("rotated-account@example.com")) + } + + @Test + func `merged store icon observation signature ignores non primary snapshot churn`() throws { + let (settings, store, controller) = self.makeController( + suiteName: "StatusItemIconObservationSignatureTests-merged-secondary-snapshot") + defer { controller.releaseStatusItemsForTesting() } + + let registry = ProviderRegistry.shared + let claudeMetadata = try #require(registry.metadata[.claude]) + settings.setProviderEnabled(provider: .claude, metadata: claudeMetadata, enabled: true) + store._setSnapshotForTesting( + Self.makeSnapshot(provider: .claude, email: "claude@example.com"), + provider: .claude) + let baseline = controller.storeIconObservationSignature() + + store._setSnapshotForTesting( + Self.makeSnapshot( + provider: .claude, + email: "changed@example.com", + primaryUsedPercent: 99, + secondaryUsedPercent: 88, + updatedAt: Date(timeIntervalSince1970: 300)), + provider: .claude) + + #expect(controller.storeIconObservationSignature() == baseline) + } + + @Test + func `store icon observation signature changes when icon percentages change`() { + let (_, store, controller) = self.makeController( + suiteName: "StatusItemIconObservationSignatureTests-percent-change") + defer { controller.releaseStatusItemsForTesting() } + + let baseline = controller.storeIconObservationSignature() + + store._setSnapshotForTesting( + Self.makeSnapshot( + provider: .codex, + email: "icon@example.com", + primaryUsedPercent: 42, + secondaryUsedPercent: 63), + provider: .codex) + + #expect(controller.storeIconObservationSignature() != baseline) + } + + @Test + func `store icon observation signature changes when credit fallback changes`() { + let (_, store, controller) = self.makeController( + suiteName: "StatusItemIconObservationSignatureTests-credit-fallback") + defer { controller.releaseStatusItemsForTesting() } + + store._setSnapshotForTesting( + Self.makeSnapshot( + provider: .codex, + email: "icon@example.com", + primaryUsedPercent: 100, + secondaryUsedPercent: 20), + provider: .codex) + store.credits = CreditsSnapshot(remaining: 80, events: [], updatedAt: Date(timeIntervalSince1970: 100)) + let baseline = controller.storeIconObservationSignature() + + store.credits = CreditsSnapshot(remaining: 42, events: [], updatedAt: Date(timeIntervalSince1970: 200)) + + #expect(controller.storeIconObservationSignature() != baseline) + } + + @Test + func `store icon observation signature ignores unused credit balance`() { + let (_, store, controller) = self.makeController( + suiteName: "StatusItemIconObservationSignatureTests-unused-credits") + defer { controller.releaseStatusItemsForTesting() } + + store.credits = CreditsSnapshot(remaining: 80, events: [], updatedAt: Date(timeIntervalSince1970: 100)) + let baseline = controller.storeIconObservationSignature() + + store.credits = CreditsSnapshot(remaining: 42, events: [], updatedAt: Date(timeIntervalSince1970: 200)) + + #expect(controller.storeIconObservationSignature() == baseline) + } + + @Test + func `merged store icon observation signature changes when non primary status changes`() throws { + let (settings, store, controller) = self.makeController( + suiteName: "StatusItemIconObservationSignatureTests-merged-secondary-status") + defer { controller.releaseStatusItemsForTesting() } + + let registry = ProviderRegistry.shared + let claudeMetadata = try #require(registry.metadata[.claude]) + settings.setProviderEnabled(provider: .claude, metadata: claudeMetadata, enabled: true) + let baseline = controller.storeIconObservationSignature() + + store.statuses[.claude] = ProviderStatus( + indicator: .major, + description: "Claude status issue", + updatedAt: Date(timeIntervalSince1970: 20)) + + #expect(controller.storeIconObservationSignature() != baseline) + } + @Test func `store icon observation signature changes when status indicator changes`() { let (_, store, controller) = self.makeController( @@ -75,11 +198,26 @@ struct StatusItemIconObservationSignatureTests { #expect(controller.storeIconObservationSignature() != baseline) } - private static func makeSnapshot(provider: UsageProvider, email: String) -> UsageSnapshot { + private static func makeSnapshot( + provider: UsageProvider, + email: String, + primaryUsedPercent: Double = 10, + secondaryUsedPercent: Double = 20, + updatedAt: Date = Date(timeIntervalSince1970: 100)) + -> UsageSnapshot + { UsageSnapshot( - primary: RateWindow(usedPercent: 10, windowMinutes: 300, resetsAt: nil, resetDescription: nil), - secondary: RateWindow(usedPercent: 20, windowMinutes: 10080, resetsAt: nil, resetDescription: nil), - updatedAt: Date(timeIntervalSince1970: 100), + primary: RateWindow( + usedPercent: primaryUsedPercent, + windowMinutes: 300, + resetsAt: nil, + resetDescription: nil), + secondary: RateWindow( + usedPercent: secondaryUsedPercent, + windowMinutes: 10080, + resetsAt: nil, + resetDescription: nil), + updatedAt: updatedAt, identity: ProviderIdentitySnapshot( providerID: provider, accountEmail: email, From fa267cb438ec1f11734708fd4ac1ccf771790739 Mon Sep 17 00:00:00 2001 From: hhh2210 Date: Thu, 4 Jun 2026 14:45:05 +0800 Subject: [PATCH 2/5] Remove residual reflection from icon observation signature A main-thread sample of the packaged build (Merge Icons on, macOS 26.5) showed providerStoreIconObservationSignature still bottoming out in String(describing:) -> _adHocPrint_unlocked reflection, from two String(describing:) calls over the payload-free IconStyle enum. Make IconStyle String-raw-represented (rawValue == case name, so the signature string is byte-identical) and replace both String(describing:) calls with .rawValue. The icon-observation leaf is now reflection-free; a re-sample shows providerStoreIconObservationSignature and _adHocPrint_unlocked gone from the path entirely. Co-Authored-By: Claude Opus 4.8 --- Sources/CodexBar/StatusItemController+IconObservation.swift | 4 ++-- Sources/CodexBarCore/Providers/Providers.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/CodexBar/StatusItemController+IconObservation.swift b/Sources/CodexBar/StatusItemController+IconObservation.swift index bbeb952ab7..a205bbc84f 100644 --- a/Sources/CodexBar/StatusItemController+IconObservation.swift +++ b/Sources/CodexBar/StatusItemController+IconObservation.swift @@ -26,7 +26,7 @@ extension StatusItemController { "merge=\(mergeIcons ? "1" : "0")", "visible=\(visibleProviders)", "primary=\(primaryProvider?.rawValue ?? "nil")", - "iconStyle=\(String(describing: self.store.iconStyle))", + "iconStyle=\(self.store.iconStyle.rawValue)", "showUsed=\(self.settings.usageBarsShowUsed ? "1" : "0")", "brandPercent=\(showBrandPercent ? "1" : "0")", "needsAnimation=\(self.needsMenuBarIconAnimation() ? "1" : "0")", @@ -48,7 +48,7 @@ extension StatusItemController { return [ provider.rawValue, - "style=\(String(describing: style))", + "style=\(style.rawValue)", "primary=\(Self.iconSignatureValue(resolved?.primary))", "weekly=\(Self.iconSignatureValue(resolved?.secondary))", "credits=\(Self.iconSignatureValue(creditsRemaining))", diff --git a/Sources/CodexBarCore/Providers/Providers.swift b/Sources/CodexBarCore/Providers/Providers.swift index e71f363700..0727067ab5 100644 --- a/Sources/CodexBarCore/Providers/Providers.swift +++ b/Sources/CodexBarCore/Providers/Providers.swift @@ -55,7 +55,7 @@ public enum UsageProvider: String, CaseIterable, Sendable, Codable { // swiftformat:enable sortDeclarations -public enum IconStyle: Sendable, CaseIterable { +public enum IconStyle: String, Sendable, CaseIterable { case codex case openai case claude From 92a606fb3fe199d67fa511e8b07f10040608ce3b Mon Sep 17 00:00:00 2001 From: hhh2210 Date: Fri, 5 Jun 2026 18:37:46 +0800 Subject: [PATCH 3/5] Derive icon credits fallback from the Codex projection, not a hand-rolled predicate menuBarCreditsRemainingForIcon (used by both the rendered menu-bar icon and the icon observation signature) reimplemented the menu-bar credits fallback with its own rate-window predicate over snapshot.primary/secondary. That is a second source of truth for a decision the Codex projection already owns (codexConsumerProjection -> menuBarFallback == .creditsBalance): equivalent today, but free to drift from the rendered/menu fallback semantics as the projection evolves. Delegate to store.codexMenuBarCreditsRemaining instead, so render, signature, and the menu-bar fallback all read one projection. The projection is pure value composition over already-loaded snapshot/credits state (no IO), so the icon/signature path stays cheap. Behavior is unchanged in the covered cases (8 icon observation signature tests still pass). Co-Authored-By: Claude Opus 4.8 --- .../StatusItemController+Animation.swift | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/Sources/CodexBar/StatusItemController+Animation.swift b/Sources/CodexBar/StatusItemController+Animation.swift index 853098d4ff..54175bb942 100644 --- a/Sources/CodexBar/StatusItemController+Animation.swift +++ b/Sources/CodexBar/StatusItemController+Animation.swift @@ -573,19 +573,17 @@ extension StatusItemController { } func menuBarCreditsRemainingForIcon(provider: UsageProvider, snapshot: UsageSnapshot?) -> Double? { - guard provider == .codex, - let creditsRemaining = self.store.credits?.remaining, - creditsRemaining > 0 - else { - return nil - } - - let rateWindows = [snapshot?.primary, snapshot?.secondary].compactMap(\.self) - guard rateWindows.isEmpty || rateWindows.contains(where: { $0.remainingPercent <= 0 }) - else { - return nil - } - return creditsRemaining + // Derive the menu-bar credits fallback from the same Codex projection path the rendered + // icon and menu use (`codexConsumerProjection` -> `menuBarFallback`), instead of a + // hand-rolled rate-window predicate. The projection is pure value composition over + // already-loaded snapshot/credits state (no IO), so this stays cheap while keeping the + // icon render, this signature input, and the menu-bar fallback semantics on a single + // source of truth — a hand-rolled approximation can silently drift from the projection + // as its fallback logic evolves. + guard provider == .codex else { return nil } + return self.store.codexMenuBarCreditsRemaining( + snapshotOverride: snapshot, + now: snapshot?.updatedAt ?? Date()) } func quotaWarningFlashActive(provider: UsageProvider, now: Date = Date()) -> Bool { From 069d518fe5f513a183f67f172c062e4f757aba0b Mon Sep 17 00:00:00 2001 From: hhh2210 Date: Fri, 5 Jun 2026 19:21:40 +0800 Subject: [PATCH 4/5] Stabilize flaky switcher coalesce test timing --- Tests/CodexBarTests/StatusMenuOpenRefreshTests.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Tests/CodexBarTests/StatusMenuOpenRefreshTests.swift b/Tests/CodexBarTests/StatusMenuOpenRefreshTests.swift index bec5681a76..7a67bf8fcd 100644 --- a/Tests/CodexBarTests/StatusMenuOpenRefreshTests.swift +++ b/Tests/CodexBarTests/StatusMenuOpenRefreshTests.swift @@ -734,10 +734,7 @@ extension StatusMenuTests { try? await Task.sleep(nanoseconds: 10_000_000) controller.deferSwitcherMenuRebuildIfStillVisible(menu, provider: .codex) - try? await Task.sleep(nanoseconds: 25_000_000) - #expect(rebuildCount == 0) - - for _ in 0..<20 where rebuildCount == 0 { + for _ in 0..<40 where rebuildCount == 0 { await Task.yield() try? await Task.sleep(nanoseconds: 5_000_000) } From cffafc0b73f725a1f072bba53ad0fa351745d845 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 5 Jun 2026 11:39:41 -0700 Subject: [PATCH 5/5] docs: add changelog for icon observation fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 340e699ca2..c47b38be2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed - Menu bar: defer merged-menu close rebuilds and cache repeated menu-card height measurements so dismissing or rapidly switching the merged dropdown avoids rebuilding SwiftUI-backed cards on the main thread (#1274, #1286). Thanks @hhh2210! +- Menu bar: observe a compact icon-state signature so merged status icons no longer redraw for provider snapshot changes that cannot affect the visible icon (#1297). Thanks @hhh2210! ## 0.32.4 — 2026-06-02