From a8952804b2b619fad12ceb468bab16437cb9f588 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:05:37 +0800 Subject: [PATCH 1/4] Fix menu bar status indicator observation for token accounts --- Sources/CodexBar/UsageStore.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/CodexBar/UsageStore.swift b/Sources/CodexBar/UsageStore.swift index 19c9488717..55d4d9f72d 100644 --- a/Sources/CodexBar/UsageStore.swift +++ b/Sources/CodexBar/UsageStore.swift @@ -56,6 +56,8 @@ extension UsageStore { _ = self.statuses _ = self.tokenSnapshotPublications _ = self.historicalPaceRevision + _ = self.accountSnapshots + _ = self.codexAccountSnapshots return 0 } From ce475ecc1b0af6df149ab8c06bc9aace610e9709 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:05:35 +0800 Subject: [PATCH 2/4] Add Observation invalidation unit tests for account snapshot collections (#2283) --- ...tusItemIconObservationSignatureTests.swift | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/Tests/CodexBarTests/StatusItemIconObservationSignatureTests.swift b/Tests/CodexBarTests/StatusItemIconObservationSignatureTests.swift index 02b23dedcc..cf06284768 100644 --- a/Tests/CodexBarTests/StatusItemIconObservationSignatureTests.swift +++ b/Tests/CodexBarTests/StatusItemIconObservationSignatureTests.swift @@ -294,6 +294,73 @@ struct StatusItemIconObservationSignatureTests { #expect(controller.storeIconObservationSignature() != baseline) } + @Test + func `store icon observation token invalidates when account snapshots change`() { + let (_, store, controller) = self.makeController( + suiteName: "StatusItemIconObservationSignatureTests-account-snapshots") + defer { controller.releaseStatusItemsForTesting() } + + nonisolated(unsafe) var invalidated = false + withObservationTracking { + _ = store.iconObservationToken + } onChange: { + invalidated = true + } + + let account = ProviderTokenAccount( + id: UUID(), + label: "Account 1", + token: "token1", + addedAt: 1, + lastUsed: nil) + + store.accountSnapshots[.claude] = [ + TokenAccountUsageSnapshot( + account: account, + snapshot: nil, + error: nil, + sourceLabel: "fixture", + cacheKey: store.tokenAccountSnapshotCacheKey(provider: .claude, account: account)), + ] + + #expect(invalidated) + } + + @Test + func `store icon observation token invalidates when codex account snapshots change`() { + let (_, store, controller) = self.makeController( + suiteName: "StatusItemIconObservationSignatureTests-codex-account-snapshots") + defer { controller.releaseStatusItemsForTesting() } + + nonisolated(unsafe) var invalidated = false + withObservationTracking { + _ = store.iconObservationToken + } onChange: { + invalidated = true + } + + let visibleAccount = CodexVisibleAccount( + id: "test-codex-account", + email: "email@example.com", + authFingerprint: nil, + storedAccountID: nil, + selectionSource: .profileHome(path: "/tmp"), + isActive: true, + isLive: true, + canReauthenticate: true, + canRemove: true) + + store.codexAccountSnapshots = [ + CodexAccountUsageSnapshot( + account: visibleAccount, + snapshot: nil, + error: nil, + sourceLabel: "cached"), + ] + + #expect(invalidated) + } + @Test func `store icon observation signature changes when hide critters toggles`() { let (settings, _, controller) = self.makeController( From 0b7e28036049c8c884040ef76dc4f859199777c9 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Thu, 30 Jul 2026 23:00:19 +0800 Subject: [PATCH 3/4] Include accountSnapshots in storeIconObservationSignature to trigger status item updates (#2283) --- ...StatusItemController+IconObservation.swift | 21 +++++++++++++++++++ ...tusItemIconObservationSignatureTests.swift | 10 +++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Sources/CodexBar/StatusItemController+IconObservation.swift b/Sources/CodexBar/StatusItemController+IconObservation.swift index 218b1f882e..eceb5ea678 100644 --- a/Sources/CodexBar/StatusItemController+IconObservation.swift +++ b/Sources/CodexBar/StatusItemController+IconObservation.swift @@ -64,9 +64,30 @@ extension StatusItemController { "text=\(displayText ?? "nil")", "layoutCost=\(layoutCostSignature ?? "nil")", "layoutAccount=\(layoutAccountSignature ?? "nil")", + "accountSnapshots=\(self.accountSnapshotSignature(for: provider))", ].joined(separator: "|") } + private func accountSnapshotSignature(for provider: UsageProvider) -> String { + let accountSnapshots = self.store.accountSnapshots[provider] ?? [] + let accountPart = accountSnapshots.map { snap in + let label = snap.account.label + return "\(snap.account.id.uuidString):\(label)" + }.joined(separator: ",") + + let codexPart: String = if provider == .codex { + self.store.codexAccountSnapshots.map { snap in + let email = snap.account.email + let active = snap.account.isActive ? "1" : "0" + return "\(snap.id):\(email):\(active)" + }.joined(separator: ",") + } else { + "" + } + + return "\(accountPart)|\(codexPart)" + } + private func storedMenuBarLayoutAccountSignature( for provider: UsageProvider, snapshot: UsageSnapshot?) diff --git a/Tests/CodexBarTests/StatusItemIconObservationSignatureTests.swift b/Tests/CodexBarTests/StatusItemIconObservationSignatureTests.swift index cf06284768..1015e64a28 100644 --- a/Tests/CodexBarTests/StatusItemIconObservationSignatureTests.swift +++ b/Tests/CodexBarTests/StatusItemIconObservationSignatureTests.swift @@ -300,6 +300,8 @@ struct StatusItemIconObservationSignatureTests { suiteName: "StatusItemIconObservationSignatureTests-account-snapshots") defer { controller.releaseStatusItemsForTesting() } + let baselineSignature = controller.storeIconObservationSignature() + nonisolated(unsafe) var invalidated = false withObservationTracking { _ = store.iconObservationToken @@ -314,16 +316,17 @@ struct StatusItemIconObservationSignatureTests { addedAt: 1, lastUsed: nil) - store.accountSnapshots[.claude] = [ + store.accountSnapshots[.codex] = [ TokenAccountUsageSnapshot( account: account, snapshot: nil, error: nil, sourceLabel: "fixture", - cacheKey: store.tokenAccountSnapshotCacheKey(provider: .claude, account: account)), + cacheKey: store.tokenAccountSnapshotCacheKey(provider: .codex, account: account)), ] #expect(invalidated) + #expect(controller.storeIconObservationSignature() != baselineSignature) } @Test @@ -332,6 +335,8 @@ struct StatusItemIconObservationSignatureTests { suiteName: "StatusItemIconObservationSignatureTests-codex-account-snapshots") defer { controller.releaseStatusItemsForTesting() } + let baselineSignature = controller.storeIconObservationSignature() + nonisolated(unsafe) var invalidated = false withObservationTracking { _ = store.iconObservationToken @@ -359,6 +364,7 @@ struct StatusItemIconObservationSignatureTests { ] #expect(invalidated) + #expect(controller.storeIconObservationSignature() != baselineSignature) } @Test From 1abe620aecce2d15af3b131cde64f73958331e14 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Fri, 31 Jul 2026 12:30:49 +0800 Subject: [PATCH 4/4] Include mutable snapshot state in store icon observation signature (#2511) The accountSnapshotSignature only tracked stable identity fields, so replacing an account snapshot's usage, error, or source label did not invalidate the observation token. Extend the signature to include the full snapshot state (primary/secondary/tertiary RateWindow details, error, sourceLabel) so icon redraws are triggered when mutable data changes. Add regression tests for: - Account snapshot usage replacement - Account snapshot error replacement - Codex account snapshot replacement Co-authored-by: OpenAI Codex Co-authored-by: Cursor --- ...StatusItemController+IconObservation.swift | 20 +++- ...tusItemIconObservationSignatureTests.swift | 109 ++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) diff --git a/Sources/CodexBar/StatusItemController+IconObservation.swift b/Sources/CodexBar/StatusItemController+IconObservation.swift index eceb5ea678..a99cf8123d 100644 --- a/Sources/CodexBar/StatusItemController+IconObservation.swift +++ b/Sources/CodexBar/StatusItemController+IconObservation.swift @@ -72,14 +72,16 @@ extension StatusItemController { let accountSnapshots = self.store.accountSnapshots[provider] ?? [] let accountPart = accountSnapshots.map { snap in let label = snap.account.label - return "\(snap.account.id.uuidString):\(label)" + let usage = Self.usageSnapshotSignature(snap.snapshot) + return "\(snap.account.id.uuidString):\(label):\(usage):\(snap.error ?? "nil"):\(snap.sourceLabel ?? "nil")" }.joined(separator: ",") let codexPart: String = if provider == .codex { self.store.codexAccountSnapshots.map { snap in let email = snap.account.email let active = snap.account.isActive ? "1" : "0" - return "\(snap.id):\(email):\(active)" + let usage = Self.usageSnapshotSignature(snap.snapshot) + return "\(snap.id):\(email):\(active):\(usage):\(snap.error ?? "nil"):\(snap.sourceLabel ?? "nil")" }.joined(separator: ",") } else { "" @@ -88,6 +90,20 @@ extension StatusItemController { return "\(accountPart)|\(codexPart)" } + private static func usageSnapshotSignature(_ snapshot: UsageSnapshot?) -> String { + guard let snapshot else { return "nil" } + return [ + Self.rateWindowSignature(snapshot.primary), + Self.rateWindowSignature(snapshot.secondary), + Self.rateWindowSignature(snapshot.tertiary), + ].joined(separator: ";") + } + + private static func rateWindowSignature(_ window: RateWindow?) -> String { + guard let window else { return "nil" } + return "\(window.usedPercent)/\(window.windowMinutes ?? -1)/\(window.resetsAt?.timeIntervalSince1970 ?? -1)" + } + private func storedMenuBarLayoutAccountSignature( for provider: UsageProvider, snapshot: UsageSnapshot?) diff --git a/Tests/CodexBarTests/StatusItemIconObservationSignatureTests.swift b/Tests/CodexBarTests/StatusItemIconObservationSignatureTests.swift index 1015e64a28..80073d460c 100644 --- a/Tests/CodexBarTests/StatusItemIconObservationSignatureTests.swift +++ b/Tests/CodexBarTests/StatusItemIconObservationSignatureTests.swift @@ -367,6 +367,115 @@ struct StatusItemIconObservationSignatureTests { #expect(controller.storeIconObservationSignature() != baselineSignature) } + @Test + func `store icon observation signature changes when existing account snapshot usage is replaced`() { + let (_, store, controller) = self.makeController( + suiteName: "StatusItemIconObservationSignatureTests-account-usage-replacement") + defer { controller.releaseStatusItemsForTesting() } + + let account = ProviderTokenAccount( + id: UUID(), + label: "Account 1", + token: "token1", + addedAt: 1, + lastUsed: nil) + let cacheKey = store.tokenAccountSnapshotCacheKey(provider: .codex, account: account) + + store.accountSnapshots[.codex] = [ + TokenAccountUsageSnapshot( + account: account, + snapshot: Self.makeSnapshot(provider: .codex, email: "account@example.com", primaryUsedPercent: 10), + error: nil, + sourceLabel: "fixture", + cacheKey: cacheKey), + ] + let baseline = controller.storeIconObservationSignature() + + store.accountSnapshots[.codex] = [ + TokenAccountUsageSnapshot( + account: account, + snapshot: Self.makeSnapshot(provider: .codex, email: "account@example.com", primaryUsedPercent: 42), + error: nil, + sourceLabel: "fixture", + cacheKey: cacheKey), + ] + + #expect(controller.storeIconObservationSignature() != baseline) + } + + @Test + func `store icon observation signature changes when existing account snapshot error is replaced`() { + let (_, store, controller) = self.makeController( + suiteName: "StatusItemIconObservationSignatureTests-account-error-replacement") + defer { controller.releaseStatusItemsForTesting() } + + let account = ProviderTokenAccount( + id: UUID(), + label: "Account 1", + token: "token1", + addedAt: 1, + lastUsed: nil) + let cacheKey = store.tokenAccountSnapshotCacheKey(provider: .codex, account: account) + + store.accountSnapshots[.codex] = [ + TokenAccountUsageSnapshot( + account: account, + snapshot: nil, + error: nil, + sourceLabel: "fixture", + cacheKey: cacheKey), + ] + let baseline = controller.storeIconObservationSignature() + + store.accountSnapshots[.codex] = [ + TokenAccountUsageSnapshot( + account: account, + snapshot: nil, + error: "rate limited", + sourceLabel: "fixture", + cacheKey: cacheKey), + ] + + #expect(controller.storeIconObservationSignature() != baseline) + } + + @Test + func `store icon observation signature changes when existing codex account snapshot is replaced`() { + let (_, store, controller) = self.makeController( + suiteName: "StatusItemIconObservationSignatureTests-codex-account-replacement") + defer { controller.releaseStatusItemsForTesting() } + + let visibleAccount = CodexVisibleAccount( + id: "test-codex-account", + email: "email@example.com", + authFingerprint: nil, + storedAccountID: nil, + selectionSource: .profileHome(path: "/tmp"), + isActive: true, + isLive: true, + canReauthenticate: true, + canRemove: true) + + store.codexAccountSnapshots = [ + CodexAccountUsageSnapshot( + account: visibleAccount, + snapshot: Self.makeSnapshot(provider: .codex, email: "email@example.com", primaryUsedPercent: 10), + error: nil, + sourceLabel: "cached"), + ] + let baseline = controller.storeIconObservationSignature() + + store.codexAccountSnapshots = [ + CodexAccountUsageSnapshot( + account: visibleAccount, + snapshot: Self.makeSnapshot(provider: .codex, email: "email@example.com", primaryUsedPercent: 66), + error: nil, + sourceLabel: "cached"), + ] + + #expect(controller.storeIconObservationSignature() != baseline) + } + @Test func `store icon observation signature changes when hide critters toggles`() { let (settings, _, controller) = self.makeController(