Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

### Fixed
- Menu bar: anchor merged provider dropdowns to the status item's trailing edge without marking preserved in-flight refresh content fresh, preventing horizontal drift while keeping deferred updates visible (#1288). Thanks @Yuxin-Qiao!
- Menu bar: avoid republishing unchanged provider storage footprints so background scans no longer trigger unnecessary menu observation work (#1416). Thanks @soohanpark!
- Cost usage: replace repeated Foundation metadata/root checks with one portable file-stat pass so expired Codex history refreshes stay responsive on very large session archives (#1392). Thanks @TheAngryPit and @ProspectOre!
- Cursor: show the Safari Full Disk Access recovery hint before the long browser login list so permission guidance remains visible when menu errors truncate (#1419, fixes #1417). Thanks @hhh2210!
- Cursor: present legacy request-based plans as one Requests quota with the raw used/limit count instead of unrelated token-based Auto/API bars (#1420, fixes #1418). Thanks @hhh2210!
Expand Down
20 changes: 18 additions & 2 deletions Sources/CodexBar/UsageStore+ProviderStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,25 @@ extension UsageStore {
updatedAt: Date)
{
let providerSet = Set(providers)
self.providerStorageFootprints = self.providerStorageFootprints.filter { !providerSet.contains($0.key) }
var updated = self.providerStorageFootprints.filter { !providerSet.contains($0.key) }
for provider in providers {
self.providerStorageFootprints[provider] = footprints[provider]
// Reuse the existing footprint when only its scan timestamp would change, so the equality
// guard below treats an unchanged scan as a no-op.
if let incoming = footprints[provider],
let existing = self.providerStorageFootprints[provider],
existing.hasSameContents(as: incoming)
{
updated[provider] = existing
} else {
updated[provider] = footprints[provider]
}
}
// Only republish the observable footprints when a value actually changed. Storage scans run
// on every menu open and roughly every 5 minutes; an unconditional re-assignment wakes
// `menuObservationToken` -> `invalidateMenus` churn (clearing menu caches) even when the
// scanned bytes are identical.
if updated != self.providerStorageFootprints {
self.providerStorageFootprints = updated
}
self.lastStorageRefreshSignature = signature
self.lastStorageRefreshRequestKey = requestKey ?? signature
Expand Down
12 changes: 12 additions & 0 deletions Sources/CodexBarCore/ProviderStorageFootprint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ public struct ProviderStorageFootprint: Sendable, Equatable {
self.totalBytes > 0
}

/// Value equality that ignores `updatedAt`. Two scans of identical on-disk data differ only by
/// their scan timestamp, so callers use this to avoid re-publishing observable state (and the
/// menu-invalidation churn that follows) when nothing the user sees has actually changed.
public func hasSameContents(as other: ProviderStorageFootprint) -> Bool {
self.provider == other.provider &&
self.totalBytes == other.totalBytes &&
self.paths == other.paths &&
self.missingPaths == other.missingPaths &&
self.unreadablePaths == other.unreadablePaths &&
self.components == other.components
}

public var cleanupRecommendations: [ProviderStorageRecommendation] {
ProviderStorageRecommendation.recommendations(for: self)
}
Expand Down
66 changes: 66 additions & 0 deletions Tests/CodexBarTests/ProviderStorageFootprintTests.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
import AppKit
import CodexBarCore
import Foundation
import Observation
import Testing
@testable import CodexBar

struct ProviderStorageFootprintTests {
private final class ObservationFlag: @unchecked Sendable {
private let lock = NSLock()
private var value = false

func set() {
self.lock.lock()
self.value = true
self.lock.unlock()
}

func get() -> Bool {
self.lock.lock()
defer { self.lock.unlock() }
return self.value
}
}

@Test
func `scanner sums nested regular files and skips symlink targets`() throws {
let root = try Self.makeTemporaryDirectory()
Expand Down Expand Up @@ -312,6 +330,54 @@ struct ProviderStorageFootprintTests {
#expect(store.storageFootprintText(for: .codex) == "No local data found")
}

@Test
@MainActor
func `repeated identical storage refresh does not republish observable footprints`() async throws {
let home = try Self.makeTemporaryDirectory()
defer { try? FileManager.default.removeItem(at: home) }

let codexHome = home.appendingPathComponent(".codex", isDirectory: true)
let sessions = codexHome.appendingPathComponent("sessions", isDirectory: true)
try FileManager.default.createDirectory(at: sessions, withIntermediateDirectories: true)
try Data(repeating: 1, count: 32).write(to: sessions.appendingPathComponent("session.jsonl"))

let suite = "ProviderStorageFootprintTests-identity-\(UUID().uuidString)"
let defaults = try #require(UserDefaults(suiteName: suite))
defaults.removePersistentDomain(forName: suite)
let settings = SettingsStore(
userDefaults: defaults,
configStore: testConfigStore(suiteName: suite),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
if let codexMetadata = ProviderDefaults.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: codexMetadata, enabled: true)
}
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
environmentBase: ["CODEX_HOME": codexHome.path])
settings.providerStorageFootprintsEnabled = true
store.managedCodexAccountsForStorageOverride = []

await store.refreshStorageFootprintsForOverviewNow()
#expect(store.storageFootprint(for: .codex)?.totalBytes == 32)

// A second scan over identical on-disk data must not re-assign the observable property.
// Storage scans run on every menu open and every ~5 min; an unconditional re-publish wakes
// `menuObservationToken` -> `invalidateMenus` churn for no value change.
let didRepublish = ObservationFlag()
withObservationTracking {
_ = store.providerStorageFootprints
} onChange: {
didRepublish.set()
}
await store.refreshStorageFootprintsForOverviewNow()

#expect(didRepublish.get() == false)
#expect(store.storageFootprint(for: .codex)?.totalBytes == 32)
}

@Test
@MainActor
func `storage refresh is opt in and clears stale footprints when disabled`() async throws {
Expand Down
Loading