Skip to content
Merged
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 @@
- Menu bar: move the highlighted Overview provider with trackpad or mouse-wheel scrolling while preserving native submenu and keyboard behavior (#1436). Thanks @joshuavial!

### Fixed
- Menu bar: avoid republishing unchanged provider storage footprints so background scans no longer trigger unnecessary menu observation work (#1416). Thanks @soohanpark!
- Claude: explain that an unauthorized Web session requires signing in at claude.ai or refreshing imported cookies (#1287). Thanks @LeoLin990405!
- CLI server: reload provider config for every usage and cost request, invalidate config-dependent cache entries, and prune expired config variants without restarting `codexbar serve`. Thanks @enieuwy!
- Menu bar: reserve quota-bar space consistently across Overview and provider switcher segments so selection no longer changes segment height (#1445). Thanks @Zihao-Qi!
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
67 changes: 67 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,55 @@ 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.refreshStorageFootprintsNow(for: [.codex])
#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
// the controller's `menuObservationToken` -> `invalidateMenus` path for no value change.
let didRepublish = ObservationFlag()
withObservationTracking {
_ = store.providerStorageFootprints
} onChange: {
didRepublish.set()
}
await store.refreshStorageFootprintsNow(for: [.codex])
try? await Task.sleep(for: .milliseconds(50))

#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