chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:33 +08:00
commit fc4fcbab58
1848 changed files with 472303 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
import AppKit
@MainActor
func showAbout() {
NSApp.activate(ignoringOtherApps: true)
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? ""
let versionString = build.isEmpty ? version : "\(version) (\(build))"
let buildTimestamp = Bundle.main.object(forInfoDictionaryKey: "CodexBuildTimestamp") as? String
let gitCommit = Bundle.main.object(forInfoDictionaryKey: "CodexGitCommit") as? String
let separator = NSAttributedString(string: " · ", attributes: [
.font: NSFont.systemFont(ofSize: NSFont.smallSystemFontSize),
])
func makeLink(_ title: String, urlString: String) -> NSAttributedString {
NSAttributedString(string: title, attributes: [
.link: URL(string: urlString) as Any,
.font: NSFont.systemFont(ofSize: NSFont.smallSystemFontSize),
])
}
let credits = NSMutableAttributedString(string: "Peter Steinberger — MIT License\n")
credits.append(makeLink("GitHub", urlString: "https://github.com/steipete/CodexBar"))
credits.append(separator)
credits.append(makeLink("Website", urlString: "https://codexbar.app"))
credits.append(separator)
credits.append(makeLink("Twitter", urlString: "https://twitter.com/steipete"))
credits.append(separator)
credits.append(makeLink("Email", urlString: "mailto:peter@steipete.me"))
if let buildTimestamp, let formatted = formattedBuildTimestamp(buildTimestamp) {
var builtLine = "Built \(formatted)"
if let gitCommit, !gitCommit.isEmpty, gitCommit != "unknown" {
builtLine += " (\(gitCommit)"
#if DEBUG
builtLine += " DEBUG BUILD"
#endif
builtLine += ")"
}
credits.append(NSAttributedString(string: "\n\(builtLine)", attributes: [
.font: NSFont.systemFont(ofSize: NSFont.smallSystemFontSize),
.foregroundColor: NSColor.secondaryLabelColor,
]))
}
let options: [NSApplication.AboutPanelOptionKey: Any] = [
.applicationName: "CodexBar",
.applicationVersion: versionString,
.version: versionString,
.credits: credits,
.applicationIcon: (NSApplication.shared.applicationIconImage ?? NSImage()) as Any,
]
NSApp.orderFrontStandardAboutPanel(options: options)
// Remove the focus ring around the app icon in the standard About panel for a cleaner look.
if let aboutPanel = NSApp.windows.first(where: { $0.className.contains("About") }) {
removeFocusRings(in: aboutPanel.contentView)
}
}
private func formattedBuildTimestamp(_ timestamp: String) -> String? {
let parser = ISO8601DateFormatter()
parser.formatOptions = [.withInternetDateTime]
guard let date = parser.date(from: timestamp) else { return timestamp }
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
formatter.locale = .current
return formatter.string(from: date)
}
@MainActor
private func removeFocusRings(in view: NSView?) {
guard let view else { return }
if let imageView = view as? NSImageView {
imageView.focusRingType = .none
}
for subview in view.subviews {
removeFocusRings(in: subview)
}
}
@@ -0,0 +1,35 @@
import AdaptiveRefreshCore
import Foundation
/// Decides how long to wait before the next automatic usage refresh.
/// Pure by construction: every signal arrives via `Input`, so the same
/// input always yields the same `Decision` with no clock or system reads.
struct AdaptiveRefreshPolicy: Sendable {
struct Input: Sendable, Equatable {
let now: Date
let lastMenuOpenAt: Date?
let lowPowerModeEnabled: Bool
let thermalState: ProcessInfo.ThermalState
}
typealias Reason = AdaptiveRefreshPolicyCore.Reason
typealias Decision = AdaptiveRefreshPolicyCore.Decision
/// Representative cadence for consumers that need a single interval but cannot reach live
/// signals (`ProviderRegistry` builds provider specs before a `UsageStore` exists). Matches
/// `warmDelay`: the steady-state cadence while the user is active, which is when
/// interval-derived heuristics such as the persistent-CLI-session idle window matter most.
static let nominalIntervalForHeuristics = AdaptiveRefreshPolicyCore.nominalIntervalForHeuristics
func nextDelay(for input: Input) -> Decision {
AdaptiveRefreshPolicyCore().nextDelay(for: AdaptiveRefreshPolicyCore.Input(
now: input.now,
lastMenuOpenAt: input.lastMenuOpenAt,
lowPowerModeEnabled: input.lowPowerModeEnabled,
thermalPressure: Self.isConstrained(input.thermalState) ? .constrained : .nominal))
}
private static func isConstrained(_ state: ProcessInfo.ThermalState) -> Bool {
state == .serious || state == .critical
}
}
+154
View File
@@ -0,0 +1,154 @@
import CodexBarCore
import Foundation
import Observation
struct AgentSessionRemoteRefreshGate {
private(set) var generation = 0
private(set) var isInFlight = false
private(set) var isPending = false
mutating func settingsDidChange() {
self.generation += 1
self.isPending = self.isInFlight
}
mutating func begin() -> Int? {
guard !self.isInFlight else {
return nil
}
self.isInFlight = true
self.isPending = false
return self.generation
}
mutating func finish(generation: Int) -> (shouldPublish: Bool, shouldRetry: Bool) {
self.isInFlight = false
let outcome = (generation == self.generation, self.isPending)
self.isPending = false
return outcome
}
}
@MainActor
@Observable
final class AgentSessionsStore {
private let settings: SettingsStore
private let localScanner: LocalAgentSessionScanner
private let remoteFetcher: RemoteSessionFetcher
@ObservationIgnored private var localRefreshTask: Task<Void, Never>?
@ObservationIgnored private var remoteRefreshTask: Task<Void, Never>?
@ObservationIgnored private var localRefreshInFlight = false
@ObservationIgnored private var remoteRefreshGate = AgentSessionRemoteRefreshGate()
@ObservationIgnored var onUpdate: (@MainActor () -> Void)?
private(set) var localSessions: [AgentSession] = []
private(set) var remoteHosts: [RemoteSessionHostResult] = []
private(set) var lastUpdatedAt: Date?
init(
settings: SettingsStore,
localScanner: LocalAgentSessionScanner = LocalAgentSessionScanner(),
remoteFetcher: RemoteSessionFetcher = RemoteSessionFetcher())
{
self.settings = settings
self.localScanner = localScanner
self.remoteFetcher = remoteFetcher
}
var totalCount: Int {
self.localSessions.count + self.remoteHosts.reduce(0) { $0 + $1.sessions.count }
}
func start() {
guard self.localRefreshTask == nil, self.remoteRefreshTask == nil else { return }
self.localRefreshTask = Task { [weak self] in
while !Task.isCancelled {
await self?.refreshLocal()
try? await Task.sleep(for: .seconds(30))
}
}
self.remoteRefreshTask = Task { [weak self] in
while !Task.isCancelled {
await self?.refreshRemote()
try? await Task.sleep(for: .seconds(60))
}
}
}
func stop() {
self.localRefreshTask?.cancel()
self.remoteRefreshTask?.cancel()
self.localRefreshTask = nil
self.remoteRefreshTask = nil
}
func settingsDidChange() {
self.remoteRefreshGate.settingsDidChange()
guard self.settings.agentSessionsEnabled else {
self.localSessions = []
self.remoteHosts = []
self.onUpdate?()
return
}
guard !SettingsStore.isRunningTests else { return }
Task { [weak self] in
await self?.refreshLocal()
await self?.refreshRemote()
}
}
func refreshOnMenuOpen() {
guard self.settings.agentSessionsEnabled, !SettingsStore.isRunningTests else { return }
Task { [weak self] in
await self?.refreshLocal()
await self?.refreshRemote()
}
}
func focus(_ session: AgentSession, remoteHost: String?) {
if let remoteHost {
Task {
await self.remoteFetcher.focus(sessionID: session.id, host: remoteHost)
}
} else {
_ = SessionWindowFocuser.focus(session)
}
}
private func refreshLocal() async {
guard self.settings.agentSessionsEnabled, !self.localRefreshInFlight else { return }
self.localRefreshInFlight = true
let sessions = await self.localScanner.scan()
self.localRefreshInFlight = false
guard !Task.isCancelled, self.settings.agentSessionsEnabled else { return }
self.localSessions = sessions
self.lastUpdatedAt = Date()
self.onUpdate?()
}
private func refreshRemote() async {
guard self.settings.agentSessionsEnabled else { return }
guard var generation = self.remoteRefreshGate.begin() else { return }
while self.settings.agentSessionsEnabled {
var hosts = self.manualHosts
await hosts.append(contentsOf: self.remoteFetcher.discoveredHosts())
let results = await self.remoteFetcher.fetch(hosts: hosts)
let outcome = self.remoteRefreshGate.finish(generation: generation)
guard !Task.isCancelled, self.settings.agentSessionsEnabled else { return }
if outcome.shouldPublish {
self.remoteHosts = results
self.lastUpdatedAt = Date()
self.onUpdate?()
}
guard outcome.shouldRetry, let nextGeneration = self.remoteRefreshGate.begin() else { return }
generation = nextGeneration
}
}
private var manualHosts: [String] {
self.settings.agentSessionsManualHosts
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
}
}
+115
View File
@@ -0,0 +1,115 @@
import CodexBarCore
import Foundation
@preconcurrency import UserNotifications
@MainActor
final class AppNotifications {
static let shared = AppNotifications()
private let centerProvider: @Sendable () -> UNUserNotificationCenter
private let logger = CodexBarLog.logger(LogCategories.notifications)
private var authorizationTask: Task<Bool, Never>?
init(centerProvider: @escaping @Sendable () -> UNUserNotificationCenter = { UNUserNotificationCenter.current() }) {
self.centerProvider = centerProvider
}
func requestAuthorizationOnStartup() {
guard !Self.isRunningUnderTests else { return }
_ = self.ensureAuthorizationTask()
}
func post(
idPrefix: String,
title: String,
body: String,
badge: NSNumber? = nil,
soundEnabled: Bool = true)
{
guard !Self.isRunningUnderTests else { return }
let center = self.centerProvider()
let logger = self.logger
Task { @MainActor in
let granted = await self.ensureAuthorized()
guard granted else {
logger.debug("not authorized; skipping post", metadata: ["prefix": idPrefix])
return
}
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = soundEnabled ? .default : nil
content.badge = badge
let request = UNNotificationRequest(
identifier: "codexbar-\(idPrefix)-\(UUID().uuidString)",
content: content,
trigger: nil)
logger.info("posting", metadata: ["prefix": idPrefix])
do {
try await center.add(request)
} catch {
let errorText = String(describing: error)
logger.error("failed to post", metadata: ["prefix": idPrefix, "error": errorText])
}
}
}
// MARK: - Private
private func ensureAuthorizationTask() -> Task<Bool, Never> {
if let authorizationTask { return authorizationTask }
let task = Task { @MainActor in
await self.requestAuthorization()
}
self.authorizationTask = task
return task
}
private func ensureAuthorized() async -> Bool {
await self.ensureAuthorizationTask().value
}
private func requestAuthorization() async -> Bool {
if let existing = await self.notificationAuthorizationStatus() {
if existing == .authorized || existing == .provisional {
return true
}
if existing == .denied {
return false
}
}
let center = self.centerProvider()
return await withCheckedContinuation { continuation in
center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
continuation.resume(returning: granted)
}
}
}
private func notificationAuthorizationStatus() async -> UNAuthorizationStatus? {
let center = self.centerProvider()
return await withCheckedContinuation { continuation in
center.getNotificationSettings { settings in
continuation.resume(returning: settings.authorizationStatus)
}
}
}
private static var isRunningUnderTests: Bool {
// Swift Testing doesn't always set XCTest env vars, and removing XCTest imports from
// the test target can make NSClassFromString("XCTestCase") return nil. If we're not
// running inside an app bundle, treat it as "tests/headless" to avoid crashes when
// accessing UNUserNotificationCenter.
if Bundle.main.bundleURL.pathExtension != "app" { return true }
let env = ProcessInfo.processInfo.environment
if env["XCTestConfigurationFilePath"] != nil { return true }
if env["TESTING_LIBRARY_VERSION"] != nil { return true }
if env["SWIFT_TESTING"] != nil { return true }
return NSClassFromString("XCTestCase") != nil
}
}
@@ -0,0 +1,11 @@
import Foundation
enum ChartBarHoverSelection {
static func accepts(distanceFromBarCenter: CGFloat, barHalfWidth: CGFloat, selectableCount: Int) -> Bool {
selectableCount <= 1 || distanceFromBarCenter <= barHalfWidth
}
static func nextCalendarDay(after date: Date, calendar: Calendar = .current) -> Date {
calendar.date(byAdding: .day, value: 1, to: date) ?? date.addingTimeInterval(86400)
}
}
+123
View File
@@ -0,0 +1,123 @@
import CodexBarCore
import Darwin
import Foundation
struct ClaudeLoginRunner {
static let loginArguments = ["auth", "login", "--claudeai"]
private static let successMarkers = ["Successfully logged in", "Login successful", "Logged in successfully"]
enum Phase {
case requesting
case waitingBrowser
}
struct Result {
enum Outcome {
case success
case timedOut
case failed(status: Int32)
case missingBinary
case launchFailed(String)
}
let outcome: Outcome
let output: String
let authLink: String?
}
static func run(
timeout: TimeInterval = 120,
binary: String = "claude",
environment: [String: String]? = nil,
onPhaseChange: @escaping @Sendable (Phase) -> Void) async -> Result
{
await Task(priority: .userInitiated) {
onPhaseChange(.requesting)
do {
let runResult = try self.runPTY(
timeout: timeout,
binary: binary,
environment: environment,
onPhaseChange: onPhaseChange)
let link = self.firstLink(in: runResult.output)
switch runResult.completion {
case .processExited(status: 0):
return Result(outcome: .success, output: runResult.output, authLink: link)
case let .processExited(status):
return Result(outcome: .failed(status: status), output: runResult.output, authLink: link)
case .outputCondition where self.successMarkers.contains(where: runResult.output.contains):
return Result(outcome: .success, output: runResult.output, authLink: link)
case .outputCondition, .idleTimeout, .deadlineExceeded:
return Result(outcome: .timedOut, output: runResult.output, authLink: link)
}
} catch LoginError.binaryNotFound {
return Result(outcome: .missingBinary, output: "", authLink: nil)
} catch let LoginError.timedOut(text) {
return Result(outcome: .timedOut, output: text, authLink: self.firstLink(in: text))
} catch {
return Result(outcome: .launchFailed(error.localizedDescription), output: "", authLink: nil)
}
}.value
}
// MARK: - PTY runner
private enum LoginError: Error {
case binaryNotFound
case timedOut(text: String)
case launchFailed(String)
}
private struct PTYRunResult {
let output: String
let completion: TTYCommandRunner.Result.Completion
}
private static func runPTY(
timeout: TimeInterval,
binary: String,
environment: [String: String]?,
onPhaseChange: @escaping @Sendable (Phase) -> Void) throws -> PTYRunResult
{
let runner = TTYCommandRunner()
var options = TTYCommandRunner.Options(rows: 50, cols: 160, timeout: timeout)
options.extraArgs = self.loginArguments
options.baseEnvironment = environment
options.stopOnURL = false // keep running until CLI confirms
options.stopOnSubstrings = self.successMarkers
options.sendOnSubstrings = ["press ENTER to open in browser": "\r"]
options.settleAfterStop = 0.35
options.returnOnEmptyProcessExit = true
do {
let result = try runner.run(
binary: binary,
send: "",
options: options,
onURLDetected: { onPhaseChange(.waitingBrowser) })
return PTYRunResult(output: result.text, completion: result.completion)
} catch TTYCommandRunner.Error.binaryNotFound {
throw LoginError.binaryNotFound
} catch TTYCommandRunner.Error.timedOut {
throw LoginError.timedOut(text: "")
} catch let TTYCommandRunner.Error.launchFailed(msg) {
throw LoginError.launchFailed(msg)
} catch {
throw LoginError.launchFailed(error.localizedDescription)
}
}
private static func firstLink(in text: String) -> String? {
let pattern = #"https?://[A-Za-z0-9._~:/?#\[\]@!$&'()*+,;=%-]+"#
guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil }
let nsRange = NSRange(text.startIndex..<text.endIndex, in: text)
guard let match = regex.firstMatch(in: text, options: [], range: nsRange),
let range = Range(match.range, in: text) else { return nil }
var url = String(text[range])
while let last = url.unicodeScalars.last,
CharacterSet(charactersIn: ".,;:)]}>\"'").contains(last)
{
url.unicodeScalars.removeLast()
}
return url
}
}
+77
View File
@@ -0,0 +1,77 @@
import AppKit
import SwiftUI
@MainActor
enum MenuPasteboardCopy {
typealias DeferredAction = @MainActor @Sendable () -> Void
typealias Scheduler = @MainActor @Sendable (@escaping DeferredAction) -> Void
typealias Writer = @MainActor @Sendable (String) -> Void
static func perform(
_ text: String,
scheduler: Scheduler = Self.schedule,
writer: @escaping Writer = Self.write,
completion: @escaping DeferredAction = {})
{
scheduler {
writer(text)
completion()
}
}
private static func schedule(_ action: @escaping DeferredAction) {
DispatchQueue.main.async(execute: action)
}
private static func write(_ text: String) {
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(text, forType: .string)
}
}
struct ClickToCopyOverlay: NSViewRepresentable {
let copyText: String
func makeNSView(context: Context) -> ClickToCopyView {
ClickToCopyView(copyText: self.copyText)
}
func updateNSView(_ nsView: ClickToCopyView, context: Context) {
// Guard against no-op writes to avoid AppKit view invalidation on every
// parent card SwiftUI diff (each MenuCardView body re-eval runs through
// .overlay { ClickToCopyOverlay(...) }, which calls updateNSView even
// when copyText is unchanged).
guard nsView.copyText != self.copyText else { return }
nsView.copyText = self.copyText
}
}
final class ClickToCopyView: NSView {
var copyText: String
private let copyAction: (String) -> Void
init(
copyText: String,
copyAction: @escaping (String) -> Void = { MenuPasteboardCopy.perform($0) })
{
self.copyText = copyText
self.copyAction = copyAction
super.init(frame: .zero)
self.wantsLayer = false
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
true
}
override func mouseDown(with event: NSEvent) {
_ = event
self.copyAction(self.copyText)
}
}
@@ -0,0 +1,180 @@
import CodexBarCore
import Foundation
enum CodexAccountHealth: Equatable {
case ok
case needsReauth
case workspaceDeactivated
case missingAuth
case unavailable
var label: String? {
switch self {
case .ok:
nil
case .needsReauth:
"Needs re-auth"
case .workspaceDeactivated:
"Workspace deactivated"
case .missingAuth:
"Missing auth"
case .unavailable:
"Unavailable"
}
}
static func status(for account: CodexVisibleAccount, error: String?) -> CodexAccountHealth {
if let error {
return self.status(forError: error)
}
if account.authenticationHealthLabel != nil {
return .missingAuth
}
return .ok
}
static func status(forError error: String) -> CodexAccountHealth {
let normalized = error.lowercased()
if normalized.contains("deactivated") {
return .workspaceDeactivated
}
if normalized.contains("expired") ||
normalized.contains("revoked") ||
normalized.contains("unauthorized") ||
normalized.contains("401")
{
return .needsReauth
}
if normalized.contains("missing"), normalized.contains("auth") {
return .missingAuth
}
return .unavailable
}
}
enum CodexAccountPresentationOrdering {
static func orderedAccounts(
_ accounts: [CodexVisibleAccount],
snapshots: [CodexAccountUsageSnapshot],
activeVisibleAccountID: String?)
-> [CodexVisibleAccount]
{
guard accounts.count > 1 else { return accounts }
let snapshotByID = Dictionary(uniqueKeysWithValues: snapshots.map { ($0.id, $0) })
let rankedAccounts = accounts.enumerated().map { index, account in
RankedAccount(
account: account,
rank: Rank(
account: account,
snapshot: snapshotByID[account.id],
activeVisibleAccountID: activeVisibleAccountID,
originalIndex: index))
}
let grouped = Dictionary(grouping: rankedAccounts, by: { Self.workspaceSortKey(for: $0.account) })
return grouped.values.sorted { lhs, rhs in
(lhs.map(\.rank).min() ?? .last) < (rhs.map(\.rank).min() ?? .last)
}.flatMap { group in
group.sorted { lhs, rhs in lhs.rank < rhs.rank }.map(\.account)
}
}
private struct RankedAccount {
let account: CodexVisibleAccount
let rank: Rank
}
private struct Rank: Comparable {
static let last = Rank(bucket: Int.max, availabilityScore: -.greatestFiniteMagnitude, originalIndex: Int.max)
let bucket: Int
let availabilityScore: Double
let displaySort: String
let originalIndex: Int
private init(bucket: Int, availabilityScore: Double, originalIndex: Int) {
self.bucket = bucket
self.availabilityScore = availabilityScore
self.displaySort = ""
self.originalIndex = originalIndex
}
init(
account: CodexVisibleAccount,
snapshot: CodexAccountUsageSnapshot?,
activeVisibleAccountID: String?,
originalIndex: Int)
{
self.originalIndex = originalIndex
self.displaySort = account.menuDisplayName.lowercased()
if account.id == activeVisibleAccountID {
self.bucket = 0
} else {
let health = CodexAccountHealth.status(for: account, error: snapshot?.error)
if health != .ok {
self.bucket = health == .missingAuth ? 4 : 3
} else if let availability = Self.availability(snapshot?.snapshot), availability <= 0 {
self.bucket = 2
} else {
self.bucket = 1
}
}
self.availabilityScore = Self.availability(snapshot?.snapshot) ?? -1
}
static func < (lhs: Rank, rhs: Rank) -> Bool {
if lhs.bucket != rhs.bucket { return lhs.bucket < rhs.bucket }
if lhs.availabilityScore != rhs.availabilityScore {
return lhs.availabilityScore > rhs.availabilityScore
}
if lhs.displaySort != rhs.displaySort { return lhs.displaySort < rhs.displaySort }
return lhs.originalIndex < rhs.originalIndex
}
private static func availability(_ snapshot: UsageSnapshot?) -> Double? {
guard let snapshot else { return nil }
let session = snapshot.primary?.remainingPercent
let weekly = snapshot.secondary?.remainingPercent
return switch (session, weekly) {
case let (.some(session), .some(weekly)):
min(session, weekly)
case let (.some(session), .none):
session
case let (.none, .some(weekly)):
weekly
case (.none, .none):
nil
}
}
}
private static func workspaceSortKey(for account: CodexVisibleAccount) -> String {
if let workspaceAccountID = account.workspaceAccountID, !workspaceAccountID.isEmpty {
return workspaceAccountID.lowercased()
}
return account.menuWorkspaceLabel?.lowercased() ?? "personal"
}
}
struct CodexAccountWorkspaceSection: Equatable {
let title: String
let accounts: [CodexVisibleAccount]
}
extension [CodexVisibleAccount] {
func codexWorkspaceSections() -> [CodexAccountWorkspaceSection] {
guard !self.isEmpty else { return [] }
var sections: [CodexAccountWorkspaceSection] = []
for account in self {
let title = account.menuWorkspaceLabel ?? "Personal"
if let index = sections.firstIndex(where: { $0.title == title }) {
var accounts = sections[index].accounts
accounts.append(account)
sections[index] = CodexAccountWorkspaceSection(title: title, accounts: accounts)
} else {
sections.append(CodexAccountWorkspaceSection(title: title, accounts: [account]))
}
}
return sections
}
}
@@ -0,0 +1,114 @@
import Foundation
import Observation
struct CodexSystemAccountPromotionUserFacingError: Error, Equatable {
let title: String
let message: String
}
@MainActor
@Observable
final class CodexAccountPromotionCoordinator {
let service: CodexAccountPromotionService
weak var managedAccountCoordinator: ManagedCodexAccountCoordinator?
private(set) var isAuthenticatingLiveAccount = false
private(set) var isPromotingSystemAccount = false
private(set) var userFacingError: CodexSystemAccountPromotionUserFacingError?
init(
service: CodexAccountPromotionService,
managedAccountCoordinator: ManagedCodexAccountCoordinator? = nil)
{
self.service = service
self.managedAccountCoordinator = managedAccountCoordinator
}
convenience init(
settingsStore: SettingsStore,
usageStore: UsageStore,
managedAccountCoordinator: ManagedCodexAccountCoordinator? = nil)
{
self.init(
service: CodexAccountPromotionService(settingsStore: settingsStore, usageStore: usageStore),
managedAccountCoordinator: managedAccountCoordinator)
}
func promote(managedAccountID: UUID)
async -> Result<CodexAccountPromotionResult, CodexSystemAccountPromotionUserFacingError>
{
self.userFacingError = nil
guard !self.isInteractionBlocked() else {
let error = Self.interactionBlockedError()
self.userFacingError = error
return .failure(error)
}
self.isPromotingSystemAccount = true
defer { self.isPromotingSystemAccount = false }
do {
let result = try await self.service.promoteManagedAccount(id: managedAccountID)
return .success(result)
} catch {
let mapped = Self.mapUserFacingError(error)
self.userFacingError = mapped
return .failure(mapped)
}
}
func clearError() {
self.userFacingError = nil
}
func setLiveReauthenticationInProgress(_ isInProgress: Bool) {
self.isAuthenticatingLiveAccount = isInProgress
}
func isInteractionBlocked() -> Bool {
self.isPromotingSystemAccount ||
self.isAuthenticatingLiveAccount ||
self.managedAccountCoordinator?.hasConflictingManagedAccountOperationInFlight == true
}
private static func interactionBlockedError() -> CodexSystemAccountPromotionUserFacingError {
CodexSystemAccountPromotionUserFacingError(
title: L("Could not switch system account"),
message: L("Finish the current managed account change before switching the system account."))
}
static func mapUserFacingError(_ error: Error) -> CodexSystemAccountPromotionUserFacingError {
let title = L("Could not switch system account")
if let error = error as? CodexAccountPromotionError {
let message = switch error {
case .targetManagedAccountNotFound:
L("That account is no longer available in CodexBar. Refresh the account list and try again.")
case .targetManagedAccountAuthMissing:
L("CodexBar could not find saved auth for that account. Re-authenticate it and try again.")
case .targetManagedAccountAuthUnreadable:
L("CodexBar could not read saved auth for that account. Re-authenticate it and try again.")
case .liveAccountUnreadable:
L("CodexBar could not read the current system account on this Mac.")
case .liveAccountMissingIdentityForPreservation:
L("CodexBar could not safely preserve the current system account before switching.")
case .liveAccountAPIKeyOnlyUnsupported:
L("CodexBar can't replace a system account that is signed in with an API key only setup.")
case .displacedLiveManagedAccountConflict:
L(
"CodexBar found another managed account that already uses the current system account. " +
"Resolve the duplicate account before switching.")
case .displacedLiveImportFailed:
L("CodexBar could not save the current system account before switching.")
case .managedStoreCommitFailed:
L("CodexBar could not update managed account storage.")
case .liveAuthSwapFailed:
L("CodexBar could not replace the live Codex auth on this Mac.")
}
return CodexSystemAccountPromotionUserFacingError(title: title, message: message)
}
return CodexSystemAccountPromotionUserFacingError(title: title, message: error.localizedDescription)
}
}
@@ -0,0 +1,277 @@
import CodexBarCore
import Foundation
private struct CodexPreparedImportedAccount {
let account: ManagedCodexAccount
let homeURL: URL
}
struct CodexDisplacedLivePreservationExecutionResult: Equatable {
let displacedLiveDisposition: CodexAccountPromotionResult.DisplacedLiveDisposition
}
@MainActor
struct CodexDisplacedLivePreservationExecutor {
private let store: any ManagedCodexAccountStoring
private let homeFactory: any ManagedCodexHomeProducing
private let fileManager: FileManager
init(
store: any ManagedCodexAccountStoring,
homeFactory: any ManagedCodexHomeProducing,
fileManager: FileManager = .default)
{
self.store = store
self.homeFactory = homeFactory
self.fileManager = fileManager
}
func execute(
plan: CodexDisplacedLivePreservationPlan,
context: PreparedPromotionContext) throws
-> CodexDisplacedLivePreservationExecutionResult
{
/*
Safety contract:
- This executor never swaps live auth. The caller must do that only after success.
- Import cleanup is best-effort and leaves no orphaned managed home on failure.
- Refresh/repair may copy auth before store commit, matching current behavior.
*/
switch plan {
case .none:
return CodexDisplacedLivePreservationExecutionResult(displacedLiveDisposition: .none)
case let .reject(reason):
throw self.error(for: reason)
case .importNew:
let importedAccount = try self.importDisplacedLiveAccount(from: context)
return try self.commitImportedAccount(importedAccount)
case let .refreshExisting(destination, _),
let .repairExisting(destination, _):
guard destination.persisted.id != context.target.persisted.id else {
throw CodexAccountPromotionError.managedStoreCommitFailed
}
let refreshed = try self.refreshExistingManagedAccount(destination, from: context)
return CodexDisplacedLivePreservationExecutionResult(
displacedLiveDisposition: .alreadyManaged(managedAccountID: refreshed.id))
}
}
private func error(for reason: CodexDisplacedLivePreservationRejectReason) -> CodexAccountPromotionError {
switch reason {
case .liveUnreadable:
.liveAccountUnreadable
case .liveAPIKeyOnlyUnsupported:
.liveAccountAPIKeyOnlyUnsupported
case .liveIdentityMissingForPreservation:
.liveAccountMissingIdentityForPreservation
case .conflictingReadableManagedHome:
.displacedLiveManagedAccountConflict
}
}
private func importDisplacedLiveAccount(
from context: PreparedPromotionContext) throws
-> CodexPreparedImportedAccount
{
guard case let .readable(liveAuthMaterial) = context.live.homeState else {
throw CodexAccountPromotionError.displacedLiveImportFailed
}
let importedHomeURL = self.homeFactory.makeHomeURL()
let importedAccountID = Self.accountID(for: importedHomeURL)
do {
try self.fileManager.createDirectory(at: importedHomeURL, withIntermediateDirectories: true)
try self.writeManagedAuthData(liveAuthMaterial.rawData, to: importedHomeURL)
guard let liveAuthIdentity = context.live.authIdentity,
let email = liveAuthIdentity.email,
liveAuthIdentity.identity != .unresolved
else {
throw CodexAccountPromotionError.liveAccountMissingIdentityForPreservation
}
let now = Date().timeIntervalSince1970
return CodexPreparedImportedAccount(
account: ManagedCodexAccount(
id: importedAccountID,
email: email,
providerAccountID: liveAuthIdentity.providerAccountID,
workspaceLabel: liveAuthIdentity.workspaceLabel,
workspaceAccountID: liveAuthIdentity.workspaceAccountID,
authFingerprint: CodexAuthFingerprint.fingerprint(data: liveAuthMaterial.rawData),
managedHomePath: importedHomeURL.path,
createdAt: now,
updatedAt: now,
lastAuthenticatedAt: now),
homeURL: importedHomeURL)
} catch let error as CodexAccountPromotionError {
try? self.removeManagedHomeIfSafe(importedHomeURL)
throw error
} catch {
try? self.removeManagedHomeIfSafe(importedHomeURL)
throw CodexAccountPromotionError.displacedLiveImportFailed
}
}
private func commitImportedAccount(_ importedAccount: CodexPreparedImportedAccount) throws
-> CodexDisplacedLivePreservationExecutionResult
{
do {
let latestManagedAccounts = try self.store.loadAccounts()
try self.store.storeAccounts(ManagedCodexAccountSet(
version: latestManagedAccounts.version,
accounts: latestManagedAccounts.accounts + [importedAccount.account]))
return try self.resolveImportedAccountAfterCommit(importedAccount)
} catch let error as CodexAccountPromotionError {
try? self.removeManagedHomeIfSafe(importedAccount.homeURL)
throw error
} catch {
try? self.removeManagedHomeIfSafe(importedAccount.homeURL)
throw CodexAccountPromotionError.managedStoreCommitFailed
}
}
private func resolveImportedAccountAfterCommit(_ importedAccount: CodexPreparedImportedAccount) throws
-> CodexDisplacedLivePreservationExecutionResult
{
let persistedManagedAccounts = try self.store.loadAccounts()
if persistedManagedAccounts.account(id: importedAccount.account.id) != nil {
return CodexDisplacedLivePreservationExecutionResult(
displacedLiveDisposition: .imported(managedAccountID: importedAccount.account.id))
}
guard let existingManagedAccount = self.repairDestination(
in: persistedManagedAccounts,
for: importedAccount.account)
else {
throw CodexAccountPromotionError.managedStoreCommitFailed
}
let repairedManagedAccount = ManagedCodexAccount(
id: existingManagedAccount.id,
email: importedAccount.account.email,
providerAccountID: importedAccount.account.providerAccountID,
workspaceLabel: importedAccount.account.workspaceLabel,
workspaceAccountID: importedAccount.account.workspaceAccountID,
authFingerprint: importedAccount.account.authFingerprint,
managedHomePath: importedAccount.homeURL.path,
createdAt: existingManagedAccount.createdAt,
updatedAt: importedAccount.account.updatedAt,
lastAuthenticatedAt: importedAccount.account.lastAuthenticatedAt)
try self.store.storeAccounts(ManagedCodexAccountSet(
version: persistedManagedAccounts.version,
accounts: persistedManagedAccounts.accounts.map { account in
guard account.id == existingManagedAccount.id else { return account }
return repairedManagedAccount
}))
if existingManagedAccount.managedHomePath != importedAccount.homeURL.path {
try? self.removeManagedHomeIfSafe(
URL(fileURLWithPath: existingManagedAccount.managedHomePath, isDirectory: true))
}
return CodexDisplacedLivePreservationExecutionResult(
displacedLiveDisposition: .alreadyManaged(managedAccountID: existingManagedAccount.id))
}
private func repairDestination(
in persistedManagedAccounts: ManagedCodexAccountSet,
for importedAccount: ManagedCodexAccount) -> ManagedCodexAccount?
{
if let providerAccountID = importedAccount.providerAccountID {
return persistedManagedAccounts.account(
email: importedAccount.email,
providerAccountID: providerAccountID)
}
let normalizedEmail = importedAccount.email
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
return persistedManagedAccounts.accounts.first {
$0.email == normalizedEmail && $0.providerAccountID == nil
}
}
private func refreshExistingManagedAccount(
_ destination: PreparedStoredManagedAccount,
from context: PreparedPromotionContext) throws
-> ManagedCodexAccount
{
guard case let .readable(liveAuthMaterial) = context.live.homeState else {
throw CodexAccountPromotionError.managedStoreCommitFailed
}
guard let liveAuthIdentity = context.live.authIdentity else {
throw CodexAccountPromotionError.liveAccountMissingIdentityForPreservation
}
do {
let latestManagedAccounts = try self.store.loadAccounts()
guard let persistedManagedAccount = latestManagedAccounts.account(id: destination.persisted.id) else {
throw CodexAccountPromotionError.managedStoreCommitFailed
}
let email = liveAuthIdentity.email
?? (liveAuthIdentity.providerAccountID != nil ? persistedManagedAccount.email : nil)
guard let email, liveAuthIdentity.identity != .unresolved else {
throw CodexAccountPromotionError.liveAccountMissingIdentityForPreservation
}
let now = Date().timeIntervalSince1970
let refreshedManagedAccount = ManagedCodexAccount(
id: persistedManagedAccount.id,
email: email,
providerAccountID: liveAuthIdentity.providerAccountID ?? persistedManagedAccount.providerAccountID,
workspaceLabel: liveAuthIdentity.workspaceLabel ?? persistedManagedAccount.workspaceLabel,
workspaceAccountID: liveAuthIdentity.workspaceAccountID ?? persistedManagedAccount.workspaceAccountID,
authFingerprint: CodexAuthFingerprint.fingerprint(data: liveAuthMaterial.rawData),
managedHomePath: persistedManagedAccount.managedHomePath,
createdAt: persistedManagedAccount.createdAt,
updatedAt: now,
lastAuthenticatedAt: now)
let refreshedHomeURL = URL(fileURLWithPath: persistedManagedAccount.managedHomePath, isDirectory: true)
do {
try self.homeFactory.validateManagedHomeForDeletion(refreshedHomeURL)
} catch {
throw CodexAccountPromotionError.displacedLiveImportFailed
}
try self.fileManager.createDirectory(at: refreshedHomeURL, withIntermediateDirectories: true)
try self.writeManagedAuthData(liveAuthMaterial.rawData, to: refreshedHomeURL)
try self.store.storeAccounts(ManagedCodexAccountSet(
version: latestManagedAccounts.version,
accounts: latestManagedAccounts.accounts.map { account in
guard account.id == persistedManagedAccount.id else { return account }
return refreshedManagedAccount
}))
return refreshedManagedAccount
} catch let error as CodexAccountPromotionError {
throw error
} catch {
throw CodexAccountPromotionError.managedStoreCommitFailed
}
}
private func writeManagedAuthData(_ data: Data, to homeURL: URL) throws {
let authFileURL = CodexAccountPromotionService.authFileURL(for: homeURL)
try data.write(to: authFileURL, options: .atomic)
try self.fileManager.setAttributes(
[.posixPermissions: NSNumber(value: Int16(0o600))],
ofItemAtPath: authFileURL.path)
}
private func removeManagedHomeIfSafe(_ homeURL: URL) throws {
try self.homeFactory.validateManagedHomeForDeletion(homeURL)
if self.fileManager.fileExists(atPath: homeURL.path) {
try self.fileManager.removeItem(at: homeURL)
}
}
private static func accountID(for homeURL: URL) -> UUID {
UUID(uuidString: homeURL.lastPathComponent) ?? UUID()
}
}
@@ -0,0 +1,190 @@
import CodexBarCore
import Foundation
enum CodexDisplacedLivePreservationNoneReason: Equatable {
case liveMissing
case targetMatchesLiveAuthIdentity
}
enum CodexDisplacedLivePreservationRejectReason: Equatable {
case liveUnreadable
case liveAPIKeyOnlyUnsupported
case liveIdentityMissingForPreservation
case conflictingReadableManagedHome
}
enum CodexDisplacedLivePreservationImportReason: Equatable {
case noExistingManagedDestination
}
enum CodexDisplacedLivePreservationRefreshReason: Equatable {
case readableHomeIdentityMatch
case readableHomeIdentityMatchUsingPersistedEmailFallback
}
enum CodexDisplacedLivePreservationRepairReason: Equatable {
case persistedProviderMatchWithMissingHome
case persistedProviderMatchWithUnreadableHome
case persistedLegacyEmailMatch
}
enum CodexDisplacedLivePreservationPlan {
case none(reason: CodexDisplacedLivePreservationNoneReason)
case reject(reason: CodexDisplacedLivePreservationRejectReason)
case importNew(reason: CodexDisplacedLivePreservationImportReason)
case refreshExisting(
destination: PreparedStoredManagedAccount,
reason: CodexDisplacedLivePreservationRefreshReason)
case repairExisting(
destination: PreparedStoredManagedAccount,
reason: CodexDisplacedLivePreservationRepairReason)
}
struct CodexDisplacedLivePreservationPlanner {
func makePlan(context: PreparedPromotionContext) -> CodexDisplacedLivePreservationPlan {
switch context.live.homeState {
case .missing:
return .none(reason: .liveMissing)
case .unreadable:
return .reject(reason: .liveUnreadable)
case .apiKeyOnly:
return .reject(reason: .liveAPIKeyOnlyUnsupported)
case .readable:
break
}
guard let liveAuthIdentity = context.live.authIdentity else {
return .reject(reason: .liveIdentityMissingForPreservation)
}
if let targetAuthIdentity = context.target.authIdentity,
CodexIdentityMatcher.matches(
targetAuthIdentity.identity,
lhsEmail: targetAuthIdentity.email,
liveAuthIdentity.identity,
rhsEmail: liveAuthIdentity.email)
{
return .none(reason: .targetMatchesLiveAuthIdentity)
}
let candidates = context.storedManagedAccounts.filter { $0.persisted.id != context.target.persisted.id }
if let destination = self.findReadableHomeMatch(in: candidates, liveAuthIdentity: liveAuthIdentity) {
let reason: CodexDisplacedLivePreservationRefreshReason =
if liveAuthIdentity.email == nil {
.readableHomeIdentityMatchUsingPersistedEmailFallback
} else {
.readableHomeIdentityMatch
}
return .refreshExisting(destination: destination, reason: reason)
}
if self.hasConflictingReadableHome(in: candidates, liveAuthIdentity: liveAuthIdentity) {
return .reject(reason: .conflictingReadableManagedHome)
}
if let repaired = self.findPersistedRepairMatch(in: candidates, liveAuthIdentity: liveAuthIdentity) {
return .repairExisting(destination: repaired.destination, reason: repaired.reason)
}
guard liveAuthIdentity.identity != .unresolved, liveAuthIdentity.email != nil else {
return .reject(reason: .liveIdentityMissingForPreservation)
}
return .importNew(reason: .noExistingManagedDestination)
}
private func findReadableHomeMatch(
in candidates: [PreparedStoredManagedAccount],
liveAuthIdentity: PreparedIdentity)
-> PreparedStoredManagedAccount?
{
candidates.first { candidate in
guard let candidateAuthIdentity = candidate.authIdentity else { return false }
return CodexIdentityMatcher.matches(
candidateAuthIdentity.identity,
lhsEmail: candidateAuthIdentity.email,
liveAuthIdentity.identity,
rhsEmail: liveAuthIdentity.email)
}
}
private func findPersistedRepairMatch(
in candidates: [PreparedStoredManagedAccount],
liveAuthIdentity: PreparedIdentity)
-> (destination: PreparedStoredManagedAccount, reason: CodexDisplacedLivePreservationRepairReason)?
{
switch liveAuthIdentity.identity {
case let .providerAccount(id):
let providerAccountID = ManagedCodexAccount.normalizeProviderAccountID(id)
if let destination = candidates.first(where: {
guard $0.persisted.providerAccountID == providerAccountID else { return false }
guard let liveEmail = liveAuthIdentity.email else { return true }
return $0.persisted.email == liveEmail
}),
let reason = self.providerRepairReason(for: destination)
{
return (destination, reason)
}
if let liveEmail = liveAuthIdentity.email,
let destination = candidates.first(where: {
$0.persisted.providerAccountID == nil && $0.persisted.email == liveEmail
})
{
return (destination, .persistedLegacyEmailMatch)
}
return nil
case let .emailOnly(normalizedEmail):
guard let destination = candidates.first(where: {
$0.persisted.providerAccountID == nil && $0.persisted.email == normalizedEmail
}) else {
return nil
}
return (destination, .persistedLegacyEmailMatch)
case .unresolved:
return nil
}
}
private func hasConflictingReadableHome(
in candidates: [PreparedStoredManagedAccount],
liveAuthIdentity: PreparedIdentity)
-> Bool
{
guard case let .providerAccount(id) = liveAuthIdentity.identity else {
return false
}
let providerAccountID = ManagedCodexAccount.normalizeProviderAccountID(id)
return candidates.contains { candidate in
guard candidate.persisted.providerAccountID == providerAccountID else { return false }
if let liveEmail = liveAuthIdentity.email, candidate.persisted.email != liveEmail {
return false
}
guard case .readable = candidate.homeState else { return false }
guard let candidateAuthIdentity = candidate.authIdentity else { return false }
return !CodexIdentityMatcher.matches(
candidateAuthIdentity.identity,
lhsEmail: candidateAuthIdentity.email,
liveAuthIdentity.identity,
rhsEmail: liveAuthIdentity.email)
}
}
private func providerRepairReason(
for destination: PreparedStoredManagedAccount)
-> CodexDisplacedLivePreservationRepairReason?
{
switch destination.homeState {
case .missing:
.persistedProviderMatchWithMissingHome
case .unreadable:
.persistedProviderMatchWithUnreadableHome
case .readable:
nil
}
}
}
@@ -0,0 +1,346 @@
import CodexBarCore
import Foundation
struct PreparedIdentity: Equatable {
let email: String?
let identity: CodexIdentity
let providerAccountID: String?
let workspaceLabel: String?
let workspaceAccountID: String?
}
struct PreparedAuthMaterial {
let homeURL: URL
let rawData: Data
let credentials: CodexOAuthCredentials
let runtimeAccount: CodexAuthBackedAccount
let authIdentity: PreparedIdentity
}
enum PreparedManagedHomeState {
case readable(PreparedAuthMaterial)
case missing(homeURL: URL)
case unreadable(homeURL: URL)
}
struct PreparedStoredManagedAccount {
let persisted: ManagedCodexAccount
let persistedIdentity: PreparedIdentity
let homeState: PreparedManagedHomeState
var authIdentity: PreparedIdentity? {
switch self.homeState {
case let .readable(authMaterial):
authMaterial.authIdentity
case .missing, .unreadable:
nil
}
}
}
enum PreparedLiveHomeState {
case missing(homeURL: URL)
case unreadable(homeURL: URL)
case apiKeyOnly(PreparedAuthMaterial)
case readable(PreparedAuthMaterial)
}
struct PreparedLiveAccount {
let homeState: PreparedLiveHomeState
var homeURL: URL {
switch self.homeState {
case let .missing(homeURL), let .unreadable(homeURL):
homeURL
case let .apiKeyOnly(authMaterial), let .readable(authMaterial):
authMaterial.homeURL
}
}
var authIdentity: PreparedIdentity? {
switch self.homeState {
case let .apiKeyOnly(authMaterial), let .readable(authMaterial):
authMaterial.authIdentity
case .missing, .unreadable:
nil
}
}
}
struct PreparedPromotionContext {
let snapshot: CodexAccountReconciliationSnapshot
let managedAccounts: ManagedCodexAccountSet
let storedManagedAccounts: [PreparedStoredManagedAccount]
let target: PreparedStoredManagedAccount
let live: PreparedLiveAccount
}
@MainActor
struct PreparedPromotionContextBuilder {
private let store: any ManagedCodexAccountStoring
private let workspaceResolver: any ManagedCodexWorkspaceResolving
private let snapshotLoader: any CodexAccountReconciliationSnapshotLoading
private let authMaterialReader: any CodexAuthMaterialReading
private let baseEnvironment: [String: String]
private let fileManager: FileManager
init(
store: any ManagedCodexAccountStoring,
workspaceResolver: any ManagedCodexWorkspaceResolving,
snapshotLoader: any CodexAccountReconciliationSnapshotLoading,
authMaterialReader: any CodexAuthMaterialReading,
baseEnvironment: [String: String] = ProcessInfo.processInfo.environment,
fileManager: FileManager = .default)
{
self.store = store
self.workspaceResolver = workspaceResolver
self.snapshotLoader = snapshotLoader
self.authMaterialReader = authMaterialReader
self.baseEnvironment = baseEnvironment
self.fileManager = fileManager
}
func build(targetID: UUID) async throws -> PreparedPromotionContext {
let snapshot = self.snapshotLoader.loadSnapshot()
let managedAccounts = try self.store.loadAccounts()
var preparedAccounts: [PreparedStoredManagedAccount] = []
preparedAccounts.reserveCapacity(managedAccounts.accounts.count)
for account in managedAccounts.accounts {
let preparedAccount = try await self.prepareStoredManagedAccount(account)
preparedAccounts.append(preparedAccount)
}
guard let target = preparedAccounts.first(where: { $0.persisted.id == targetID }) else {
throw CodexAccountPromotionError.targetManagedAccountNotFound
}
let live = await self.prepareLiveAccount()
return PreparedPromotionContext(
snapshot: snapshot,
managedAccounts: managedAccounts,
storedManagedAccounts: preparedAccounts,
target: target,
live: live)
}
private func prepareStoredManagedAccount(
_ account: ManagedCodexAccount) async throws
-> PreparedStoredManagedAccount
{
let homeURL = URL(fileURLWithPath: account.managedHomePath, isDirectory: true)
let persistedIdentity = Self.persistedIdentity(from: account)
let homeState = await self.prepareManagedHomeState(homeURL: homeURL)
return PreparedStoredManagedAccount(
persisted: account,
persistedIdentity: persistedIdentity,
homeState: homeState)
}
private func prepareManagedHomeState(homeURL: URL) async -> PreparedManagedHomeState {
let readResult = self.readAuthData(homeURL: homeURL)
switch readResult {
case .missing:
return .missing(homeURL: homeURL)
case .unreadable:
return .unreadable(homeURL: homeURL)
case let .readable(rawData):
guard let authMaterial = await self.inspectAuthMaterial(homeURL: homeURL, rawData: rawData) else {
return .unreadable(homeURL: homeURL)
}
return .readable(authMaterial)
}
}
private func prepareLiveAccount() async -> PreparedLiveAccount {
let liveHomeURL = self.liveHomeURL()
let readResult = self.readAuthData(homeURL: liveHomeURL)
switch readResult {
case .missing:
return PreparedLiveAccount(homeState: .missing(homeURL: liveHomeURL))
case .unreadable:
return PreparedLiveAccount(homeState: .unreadable(homeURL: liveHomeURL))
case let .readable(rawData):
guard let authMaterial = await self.inspectAuthMaterial(homeURL: liveHomeURL, rawData: rawData) else {
return PreparedLiveAccount(homeState: .unreadable(homeURL: liveHomeURL))
}
if Self.isAPIKeyOnly(credentials: authMaterial.credentials, rawData: authMaterial.rawData) {
return PreparedLiveAccount(homeState: .apiKeyOnly(authMaterial))
}
return PreparedLiveAccount(homeState: .readable(authMaterial))
}
}
private func inspectAuthMaterial(homeURL: URL, rawData: Data) async -> PreparedAuthMaterial? {
guard let credentials = try? CodexOAuthCredentialsStore.parse(data: rawData),
let runtimeAccount = try? Self.runtimeAccount(from: rawData)
else {
return nil
}
let authIdentity = await self.derivedIdentity(
homePath: homeURL.path,
runtimeAccount: runtimeAccount)
return PreparedAuthMaterial(
homeURL: homeURL,
rawData: rawData,
credentials: credentials,
runtimeAccount: runtimeAccount,
authIdentity: authIdentity)
}
private func derivedIdentity(homePath: String, runtimeAccount: CodexAuthBackedAccount) async -> PreparedIdentity {
let normalizedEmail = Self.normalizeEmail(runtimeAccount.email)
let normalizedIdentity = Self.normalizedIdentity(runtimeAccount.identity, email: normalizedEmail)
let providerAccountID: String? = switch normalizedIdentity {
case let .providerAccount(id):
ManagedCodexAccount.normalizeProviderAccountID(id)
case .emailOnly, .unresolved:
nil
}
let workspaceIdentity: CodexOpenAIWorkspaceIdentity? = if let providerAccountID {
await self.workspaceResolver.resolveWorkspaceIdentity(
homePath: homePath,
providerAccountID: providerAccountID)
} else {
nil
}
return PreparedIdentity(
email: normalizedEmail,
identity: normalizedIdentity,
providerAccountID: providerAccountID,
workspaceLabel: workspaceIdentity?.workspaceLabel,
workspaceAccountID: workspaceIdentity?.workspaceAccountID ?? providerAccountID)
}
private static func persistedIdentity(from account: ManagedCodexAccount) -> PreparedIdentity {
let normalizedEmail = Self.normalizeEmail(account.email)
let providerAccountID = ManagedCodexAccount.normalizeProviderAccountID(account.providerAccountID)
let identity = Self.normalizedIdentity(
CodexIdentityResolver.resolve(accountId: providerAccountID, email: normalizedEmail),
email: normalizedEmail)
return PreparedIdentity(
email: normalizedEmail,
identity: identity,
providerAccountID: providerAccountID,
workspaceLabel: account.workspaceLabel,
workspaceAccountID: account.workspaceAccountID)
}
private func liveHomeURL() -> URL {
CodexHomeScope.ambientHomeURL(env: self.baseEnvironment, fileManager: self.fileManager)
}
private func readAuthData(homeURL: URL) -> PreparedAuthReadState {
do {
let rawData = try self.authMaterialReader.readAuthData(homeURL: homeURL)
guard let rawData else {
return .missing
}
return .readable(rawData)
} catch {
return .unreadable
}
}
private static func runtimeAccount(from rawData: Data) throws -> CodexAuthBackedAccount {
guard let json = try JSONSerialization.jsonObject(with: rawData) as? [String: Any] else {
throw CodexOAuthCredentialsError.decodeFailed("Invalid JSON")
}
let tokens = json["tokens"] as? [String: Any]
let idToken = tokens.flatMap {
Self.nonEmptyString(in: $0, snakeCaseKey: "id_token", camelCaseKey: "idToken")
}
let payload = idToken.flatMap(UsageFetcher.parseJWT)
let authDict = payload?["https://api.openai.com/auth"] as? [String: Any]
let profileDict = payload?["https://api.openai.com/profile"] as? [String: Any]
let email = Self.normalizeEmail(
(payload?["email"] as? String) ?? (profileDict?["email"] as? String))
let plan = Self.normalizedField(
(authDict?["chatgpt_plan_type"] as? String) ?? (payload?["chatgpt_plan_type"] as? String))
let accountID = ManagedCodexAccount.normalizeProviderAccountID(
tokens.flatMap {
Self.nonEmptyString(in: $0, snakeCaseKey: "account_id", camelCaseKey: "accountId")
}
?? (authDict?["chatgpt_account_id"] as? String)
?? (payload?["chatgpt_account_id"] as? String))
let identity = Self.normalizedIdentity(
CodexIdentityResolver.resolve(accountId: accountID, email: email),
email: email)
return CodexAuthBackedAccount(identity: identity, email: email, plan: plan)
}
private static func normalizedField(_ value: String?) -> String? {
guard let value = value?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else {
return nil
}
return value
}
private static func normalizeEmail(_ email: String?) -> String? {
CodexIdentityResolver.normalizeEmail(email)
}
private static func normalizedIdentity(_ identity: CodexIdentity, email: String?) -> CodexIdentity {
guard let email else { return identity }
return CodexIdentityMatcher.normalized(identity, fallbackEmail: email)
}
private static func isAPIKeyOnly(credentials: CodexOAuthCredentials, rawData: Data) -> Bool {
guard self.hasUsableOAuthTokens(in: rawData) == false else {
return false
}
return credentials.refreshToken.isEmpty
&& credentials.idToken == nil
&& credentials.accountId == nil
&& credentials.lastRefresh == nil
}
private static func hasUsableOAuthTokens(in rawData: Data) -> Bool {
guard let json = try? JSONSerialization.jsonObject(with: rawData) as? [String: Any],
let tokens = json["tokens"] as? [String: Any]
else {
return false
}
let accessToken = self.nonEmptyString(
in: tokens,
snakeCaseKey: "access_token",
camelCaseKey: "accessToken")
let refreshToken = self.nonEmptyString(
in: tokens,
snakeCaseKey: "refresh_token",
camelCaseKey: "refreshToken")
return accessToken != nil && refreshToken != nil
}
private static func nonEmptyString(
in dictionary: [String: Any],
snakeCaseKey: String,
camelCaseKey: String)
-> String?
{
if let value = dictionary[snakeCaseKey] as? String,
value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
{
return value
}
if let value = dictionary[camelCaseKey] as? String,
value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
{
return value
}
return nil
}
}
private enum PreparedAuthReadState {
case missing
case unreadable
case readable(Data)
}
@@ -0,0 +1,311 @@
import CodexBarCore
import Darwin
import Foundation
@MainActor
protocol CodexAccountReconciliationSnapshotLoading {
func loadSnapshot() -> CodexAccountReconciliationSnapshot
}
protocol CodexAuthMaterialReading: Sendable {
func readAuthData(homeURL: URL) throws -> Data?
}
protocol CodexLiveAuthSwapping: Sendable {
func swapLiveAuthData(_ data: Data, liveHomeURL: URL) throws
}
@MainActor
protocol CodexActiveSourceWriting {
func writeCodexActiveSource(_ source: CodexActiveSource)
}
@MainActor
protocol CodexAccountScopedRefreshing {
func refreshCodexAccountScopedState(allowDisabled: Bool) async
}
@MainActor
struct SettingsStoreCodexAccountReconciliationSnapshotLoader: CodexAccountReconciliationSnapshotLoading {
private let settingsStore: SettingsStore
init(settingsStore: SettingsStore) {
self.settingsStore = settingsStore
}
func loadSnapshot() -> CodexAccountReconciliationSnapshot {
self.settingsStore.codexAccountReconciliationSnapshot
}
}
struct DefaultCodexAuthMaterialReader: CodexAuthMaterialReading {
func readAuthData(homeURL: URL) throws -> Data? {
let authFileURL = CodexAccountPromotionService.authFileURL(for: homeURL)
guard FileManager.default.fileExists(atPath: authFileURL.path) else {
return nil
}
return try Data(contentsOf: authFileURL)
}
}
struct DefaultCodexLiveAuthSwapper: CodexLiveAuthSwapping {
func swapLiveAuthData(_ data: Data, liveHomeURL: URL) throws {
try FileManager.default.createDirectory(at: liveHomeURL, withIntermediateDirectories: true)
let liveAuthURL = CodexAccountPromotionService.authFileURL(for: liveHomeURL)
let stagedAuthURL = liveHomeURL.appendingPathComponent(
"auth.json.codexbar-staged-\(UUID().uuidString)",
isDirectory: false)
do {
try data.write(to: stagedAuthURL)
try FileManager.default.setAttributes(
[.posixPermissions: NSNumber(value: Int16(0o600))],
ofItemAtPath: stagedAuthURL.path)
try self.renameItem(at: stagedAuthURL, to: liveAuthURL)
} catch {
try? FileManager.default.removeItem(at: stagedAuthURL)
throw error
}
}
private func renameItem(at sourceURL: URL, to destinationURL: URL) throws {
let sourcePath = sourceURL.path
let destinationPath = destinationURL.path
let result = sourcePath.withCString { sourceFS in
destinationPath.withCString { destinationFS in
rename(sourceFS, destinationFS)
}
}
guard result == 0 else {
throw NSError(
domain: NSPOSIXErrorDomain,
code: Int(errno),
userInfo: [NSFilePathErrorKey: destinationPath])
}
}
}
@MainActor
struct SettingsStoreCodexActiveSourceWriter: CodexActiveSourceWriting {
private let settingsStore: SettingsStore
init(settingsStore: SettingsStore) {
self.settingsStore = settingsStore
}
func writeCodexActiveSource(_ source: CodexActiveSource) {
self.settingsStore.codexActiveSource = source
}
}
@MainActor
struct UsageStoreCodexAccountScopedRefresher: CodexAccountScopedRefreshing {
private let usageStore: UsageStore
init(usageStore: UsageStore) {
self.usageStore = usageStore
}
func refreshCodexAccountScopedState(allowDisabled: Bool) async {
await self.usageStore.refreshCodexAccountScopedState(allowDisabled: allowDisabled)
}
}
struct CodexAccountPromotionResult: Equatable {
enum Outcome: Equatable {
case promoted
case convergedNoOp
}
enum DisplacedLiveDisposition: Equatable {
case none
case alreadyManaged(managedAccountID: UUID)
case imported(managedAccountID: UUID)
}
let targetManagedAccountID: UUID
let outcome: Outcome
let displacedLiveDisposition: DisplacedLiveDisposition
let didMutateLiveAuth: Bool
let resultingActiveSource: CodexActiveSource
}
enum CodexAccountPromotionError: Error, Equatable {
case targetManagedAccountNotFound
case targetManagedAccountAuthMissing
case targetManagedAccountAuthUnreadable
case liveAccountUnreadable
case liveAccountMissingIdentityForPreservation
case liveAccountAPIKeyOnlyUnsupported
case displacedLiveManagedAccountConflict
case displacedLiveImportFailed
case managedStoreCommitFailed
case liveAuthSwapFailed
}
@MainActor
final class CodexAccountPromotionService {
private let store: any ManagedCodexAccountStoring
private let homeFactory: any ManagedCodexHomeProducing
private let identityReader: any ManagedCodexIdentityReading
private let workspaceResolver: any ManagedCodexWorkspaceResolving
private let snapshotLoader: any CodexAccountReconciliationSnapshotLoading
private let authMaterialReader: any CodexAuthMaterialReading
private let liveAuthSwapper: any CodexLiveAuthSwapping
private let activeSourceWriter: any CodexActiveSourceWriting
private let accountScopedRefresher: any CodexAccountScopedRefreshing
private let baseEnvironment: [String: String]
private let fileManager: FileManager
init(
store: any ManagedCodexAccountStoring,
homeFactory: any ManagedCodexHomeProducing,
identityReader: any ManagedCodexIdentityReading,
workspaceResolver: any ManagedCodexWorkspaceResolving = DefaultManagedCodexWorkspaceResolver(),
snapshotLoader: any CodexAccountReconciliationSnapshotLoading,
authMaterialReader: any CodexAuthMaterialReading,
liveAuthSwapper: any CodexLiveAuthSwapping,
activeSourceWriter: any CodexActiveSourceWriting,
accountScopedRefresher: any CodexAccountScopedRefreshing,
baseEnvironment: [String: String] = ProcessInfo.processInfo.environment,
fileManager: FileManager = .default)
{
self.store = store
self.homeFactory = homeFactory
self.identityReader = identityReader
self.workspaceResolver = workspaceResolver
self.snapshotLoader = snapshotLoader
self.authMaterialReader = authMaterialReader
self.liveAuthSwapper = liveAuthSwapper
self.activeSourceWriter = activeSourceWriter
self.accountScopedRefresher = accountScopedRefresher
self.baseEnvironment = baseEnvironment
self.fileManager = fileManager
}
convenience init(
settingsStore: SettingsStore,
usageStore: UsageStore,
baseEnvironment: [String: String] = ProcessInfo.processInfo.environment,
fileManager: FileManager = .default)
{
self.init(
store: FileManagedCodexAccountStore(fileManager: fileManager),
homeFactory: ManagedCodexHomeFactory(fileManager: fileManager),
identityReader: DefaultManagedCodexIdentityReader(),
workspaceResolver: DefaultManagedCodexWorkspaceResolver(),
snapshotLoader: SettingsStoreCodexAccountReconciliationSnapshotLoader(settingsStore: settingsStore),
authMaterialReader: DefaultCodexAuthMaterialReader(),
liveAuthSwapper: DefaultCodexLiveAuthSwapper(),
activeSourceWriter: SettingsStoreCodexActiveSourceWriter(settingsStore: settingsStore),
accountScopedRefresher: UsageStoreCodexAccountScopedRefresher(usageStore: usageStore),
baseEnvironment: baseEnvironment,
fileManager: fileManager)
}
func promoteManagedAccount(id: UUID) async throws -> CodexAccountPromotionResult {
let contextBuilder = PreparedPromotionContextBuilder(
store: self.store,
workspaceResolver: self.workspaceResolver,
snapshotLoader: self.snapshotLoader,
authMaterialReader: self.authMaterialReader,
baseEnvironment: self.baseEnvironment,
fileManager: self.fileManager)
let context = try await contextBuilder.build(targetID: id)
if let resultingActiveSource = self.convergedActiveSource(for: context) {
self.activeSourceWriter.writeCodexActiveSource(resultingActiveSource)
await self.accountScopedRefresher.refreshCodexAccountScopedState(allowDisabled: true)
return CodexAccountPromotionResult(
targetManagedAccountID: id,
outcome: .convergedNoOp,
displacedLiveDisposition: .none,
didMutateLiveAuth: false,
resultingActiveSource: resultingActiveSource)
}
let targetAuthMaterial = try self.requiredTargetAuthMaterial(from: context.target)
let preservationPlan = CodexDisplacedLivePreservationPlanner().makePlan(context: context)
let executionResult = try CodexDisplacedLivePreservationExecutor(
store: self.store,
homeFactory: self.homeFactory,
fileManager: self.fileManager)
.execute(plan: preservationPlan, context: context)
do {
try self.liveAuthSwapper.swapLiveAuthData(targetAuthMaterial.rawData, liveHomeURL: context.live.homeURL)
} catch {
throw CodexAccountPromotionError.liveAuthSwapFailed
}
self.activeSourceWriter.writeCodexActiveSource(.liveSystem)
await self.accountScopedRefresher.refreshCodexAccountScopedState(allowDisabled: true)
return CodexAccountPromotionResult(
targetManagedAccountID: id,
outcome: .promoted,
displacedLiveDisposition: executionResult.displacedLiveDisposition,
didMutateLiveAuth: true,
resultingActiveSource: .liveSystem)
}
nonisolated static func authFileURL(for homeURL: URL) -> URL {
homeURL.appendingPathComponent("auth.json", isDirectory: false)
}
private func convergedActiveSource(for context: PreparedPromotionContext) -> CodexActiveSource? {
if let liveAuthIdentity = context.live.authIdentity {
let targetIdentity = context.target.authIdentity ?? context.target.persistedIdentity
guard CodexIdentityMatcher.matches(
targetIdentity.identity,
lhsEmail: targetIdentity.email,
liveAuthIdentity.identity,
rhsEmail: liveAuthIdentity.email)
else {
return nil
}
if liveAuthIdentity.email != nil {
return .liveSystem
}
if liveAuthIdentity.providerAccountID != nil {
return .managedAccount(id: context.target.persisted.id)
}
return nil
}
guard let liveSystemAccount = context.snapshot.liveSystemAccount else {
return nil
}
guard CodexIdentityMatcher.matches(
context.snapshot.runtimeIdentity(for: context.target.persisted),
lhsEmail: context.snapshot.runtimeEmail(for: context.target.persisted),
context.snapshot.runtimeIdentity(for: liveSystemAccount),
rhsEmail: liveSystemAccount.email)
else {
return nil
}
return .liveSystem
}
private func requiredTargetAuthMaterial(from target: PreparedStoredManagedAccount) throws -> PreparedAuthMaterial {
switch target.homeState {
case let .readable(authMaterial):
guard authMaterial.authIdentity.email != nil else {
throw CodexAccountPromotionError.targetManagedAccountAuthUnreadable
}
return authMaterial
case .missing:
throw CodexAccountPromotionError.targetManagedAccountAuthMissing
case .unreadable:
throw CodexAccountPromotionError.targetManagedAccountAuthUnreadable
}
}
}
@@ -0,0 +1,4 @@
import CodexBarCore
typealias CodexVisibleAccount = CodexBarCore.CodexVisibleAccount
typealias CodexVisibleAccountProjection = CodexBarCore.CodexVisibleAccountProjection
@@ -0,0 +1,133 @@
import CodexBarCore
import Foundation
protocol CodexAccountUsageSnapshotStoring: Sendable {
func load(for accounts: [CodexVisibleAccount]) -> [CodexAccountUsageSnapshot]
func store(_ snapshots: [CodexAccountUsageSnapshot])
}
struct FileCodexAccountUsageSnapshotStore: CodexAccountUsageSnapshotStoring, @unchecked Sendable {
private struct Payload: Codable {
let version: Int
let records: [Record]
}
private struct Record: Codable {
let id: String
let accountIdentity: AccountIdentity?
let snapshot: UsageSnapshot?
let error: String?
let sourceLabel: String?
}
private struct AccountIdentity: Codable, Equatable {
let normalizedEmail: String?
let workspaceAccountID: String?
let authFingerprint: String?
let storedAccountID: UUID?
let selectionSource: CodexActiveSource?
init(account: CodexVisibleAccount) {
self.normalizedEmail = CodexIdentityResolver.normalizeEmail(account.email)
self.workspaceAccountID = CodexOpenAIWorkspaceResolver.normalizeWorkspaceAccountID(
account.workspaceAccountID)
self.authFingerprint = CodexAuthFingerprint.normalize(account.authFingerprint)
self.storedAccountID = account.storedAccountID
self.selectionSource = account.selectionSource
}
func matches(_ account: CodexVisibleAccount) -> Bool {
guard let normalizedEmail = self.normalizedEmail,
normalizedEmail == CodexIdentityResolver.normalizeEmail(account.email),
let workspaceAccountID = self.workspaceAccountID,
workspaceAccountID == CodexOpenAIWorkspaceResolver.normalizeWorkspaceAccountID(
account.workspaceAccountID)
else {
return false
}
return true
}
}
private static let currentVersion = 1
private let fileURL: URL
private let fileManager: FileManager
init(fileURL: URL = Self.defaultURL(), fileManager: FileManager = .default) {
self.fileURL = fileURL
self.fileManager = fileManager
}
func load(for accounts: [CodexVisibleAccount]) -> [CodexAccountUsageSnapshot] {
guard self.fileManager.fileExists(atPath: self.fileURL.path),
let data = try? Data(contentsOf: self.fileURL),
let payload = try? JSONDecoder().decode(Payload.self, from: data),
payload.version == Self.currentVersion
else {
return []
}
let accountsByID = Dictionary(uniqueKeysWithValues: accounts.map { ($0.id, $0) })
return payload.records.compactMap { record in
guard let account = accountsByID[record.id] else { return nil }
guard record.accountIdentity?.matches(account) == true else { return nil }
return CodexAccountUsageSnapshot(
account: account,
snapshot: Self.relabelSnapshot(record.snapshot, for: account),
error: record.error,
sourceLabel: record.sourceLabel)
}
}
func store(_ snapshots: [CodexAccountUsageSnapshot]) {
let payload = Payload(
version: Self.currentVersion,
records: snapshots.compactMap { snapshot in
let identity = AccountIdentity(account: snapshot.account)
guard identity.normalizedEmail != nil, identity.workspaceAccountID != nil else { return nil }
return Record(
id: snapshot.id,
accountIdentity: identity,
snapshot: snapshot.snapshot,
error: snapshot.error,
sourceLabel: snapshot.sourceLabel)
})
let directory = self.fileURL.deletingLastPathComponent()
do {
if !self.fileManager.fileExists(atPath: directory.path) {
try self.fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
}
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
try encoder.encode(payload).write(to: self.fileURL, options: [.atomic])
#if os(macOS)
try self.fileManager.setAttributes([
.posixPermissions: NSNumber(value: Int16(0o600)),
], ofItemAtPath: self.fileURL.path)
#endif
} catch {
// Snapshot hydration is best-effort; never make menu refresh fail because disk cache failed.
}
}
private static func relabelSnapshot(_ snapshot: UsageSnapshot?, for account: CodexVisibleAccount)
-> UsageSnapshot?
{
guard let snapshot else { return nil }
let identity = snapshot.identity(for: .codex)
return snapshot.withIdentity(ProviderIdentitySnapshot(
providerID: .codex,
accountEmail: account.email,
accountOrganization: identity?.accountOrganization,
loginMethod: identity?.loginMethod ?? account.workspaceLabel))
}
static func defaultURL() -> URL {
let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
?? FileManager.default.homeDirectoryForCurrentUser
return base
.appendingPathComponent("CodexBar", isDirectory: true)
.appendingPathComponent("codex-account-snapshots.json", isDirectory: false)
}
}
@@ -0,0 +1,139 @@
import CodexBarCore
import CryptoKit
import Foundation
enum CodexHistoryPersistedOwner: Equatable {
case canonical(String)
case legacyEmailHash(String)
case legacyOpaqueScoped(String)
case legacyUnscoped
}
enum CodexHistoryOwnership {
private static let providerAccountPrefix = "codex:v1:provider-account:"
private static let emailHashPrefix = "codex:v1:email-hash:"
static func canonicalKey(for identity: CodexIdentity) -> String? {
switch identity {
case let .providerAccount(id):
guard let normalized = Self.normalizeScopedValue(id) else { return nil }
return "\(Self.providerAccountPrefix)\(normalized)"
case let .emailOnly(normalizedEmail):
guard let normalized = CodexIdentityResolver.normalizeEmail(normalizedEmail) else { return nil }
return self.canonicalEmailHashKey(for: normalized)
case .unresolved:
return nil
}
}
static func canonicalEmailHashKey(for normalizedEmail: String) -> String {
"\(self.emailHashPrefix)\(self.legacyEmailHash(normalizedEmail: normalizedEmail))"
}
static func legacyEmailHash(normalizedEmail: String) -> String {
guard let normalized = CodexIdentityResolver.normalizeEmail(normalizedEmail) else { return "" }
return self.sha256Hex(normalized)
}
static func classifyPersistedKey(
_ rawKey: String?,
legacyEmailHash: String? = nil) -> CodexHistoryPersistedOwner
{
guard let normalizedKey = normalizeScopedValue(rawKey) else {
return .legacyUnscoped
}
if self.isCanonicalKey(normalizedKey) {
return .canonical(normalizedKey)
}
if let legacyEmailHash, normalizedKey == legacyEmailHash {
return .legacyEmailHash(normalizedKey)
}
return .legacyOpaqueScoped(normalizedKey)
}
static func belongsToTargetContinuity(
_ owner: CodexHistoryPersistedOwner,
targetCanonicalKey: String,
canonicalEmailHashKey: String?) -> Bool
{
switch owner {
case let .canonical(key):
if key == targetCanonicalKey {
return true
}
guard let canonicalEmailHashKey, self.isCanonicalEmailHashKey(canonicalEmailHashKey) else {
return false
}
return key == canonicalEmailHashKey
case .legacyEmailHash:
guard let canonicalEmailHashKey, self.isCanonicalEmailHashKey(canonicalEmailHashKey) else {
return false
}
return canonicalEmailHashKey == targetCanonicalKey ||
targetCanonicalKey.hasPrefix(self.providerAccountPrefix)
case .legacyOpaqueScoped, .legacyUnscoped:
return false
}
}
static func hasStrictSingleAccountContinuity(
scopedRawKeys: [String],
targetCanonicalKey: String,
canonicalEmailHashKey: String?,
legacyEmailHash: String?,
hasAdjacentMultiAccountVeto: Bool) -> Bool
{
guard !hasAdjacentMultiAccountVeto else { return false }
let normalizedCandidates = Set(scopedRawKeys.compactMap { rawKey in
let owner = self.classifyPersistedKey(rawKey, legacyEmailHash: legacyEmailHash)
if self.belongsToTargetContinuity(
owner,
targetCanonicalKey: targetCanonicalKey,
canonicalEmailHashKey: canonicalEmailHashKey)
{
return targetCanonicalKey
}
switch owner {
case .legacyUnscoped:
return nil
case let .legacyOpaqueScoped(key):
return "legacy-opaque:\(key)"
case let .legacyEmailHash(hash):
return "legacy-email-hash:\(hash)"
case let .canonical(key):
return key
}
})
guard normalizedCandidates.count == 1, normalizedCandidates.first == targetCanonicalKey else {
return false
}
return true
}
private static func isCanonicalKey(_ rawKey: String) -> Bool {
self.isCanonicalProviderAccountKey(rawKey) || self.isCanonicalEmailHashKey(rawKey)
}
static func isCanonicalProviderAccountKey(_ rawKey: String) -> Bool {
rawKey.hasPrefix(self.providerAccountPrefix) && rawKey.count > self.providerAccountPrefix.count
}
private static func isCanonicalEmailHashKey(_ rawKey: String) -> Bool {
rawKey.hasPrefix(self.emailHashPrefix) && rawKey.count > self.emailHashPrefix.count
}
private static func normalizeScopedValue(_ value: String?) -> String? {
guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty else {
return nil
}
return trimmed
}
private static func sha256Hex(_ input: String) -> String {
let digest = SHA256.hash(data: Data(input.utf8))
return digest.map { String(format: "%02x", $0) }.joined()
}
}
@@ -0,0 +1,44 @@
import Foundation
struct CodexLoginAlertInfo: Equatable {
let title: String
let message: String
}
enum CodexLoginAlertPresentation {
static func alertInfo(for result: CodexLoginRunner.Result) -> CodexLoginAlertInfo? {
switch result.outcome {
case .success:
return nil
case .missingBinary:
return CodexLoginAlertInfo(
title: L("Codex CLI not found"),
message: L("Install the Codex CLI (npm i -g @openai/codex) and try again."))
case let .launchFailed(message):
return CodexLoginAlertInfo(title: L("Could not start codex login"), message: message)
case .timedOut:
return CodexLoginAlertInfo(
title: L("Codex login timed out"),
message: self.trimmedOutput(result.output))
case let .failed(status):
let statusLine = String(format: L("codex login exited with status %d."), status)
let message = self.trimmedOutput(result.output.isEmpty ? statusLine : result.output)
return CodexLoginAlertInfo(title: L("Codex login failed"), message: message)
}
}
static func managedLoginFailureMessage(for result: CodexLoginRunner.Result) -> String {
let baseMessage = L("managed_login_failed")
guard let info = self.alertInfo(for: result) else { return baseMessage }
return "\(baseMessage)\n\n\(L("codex_login_output"))\n\(info.message)"
}
private static func trimmedOutput(_ text: String) -> String {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
let limit = 600
if trimmed.isEmpty { return L("No output captured.") }
if trimmed.count <= limit { return trimmed }
let idx = trimmed.index(trimmed.startIndex, offsetBy: limit)
return "\(trimmed[..<idx])"
}
}
+193
View File
@@ -0,0 +1,193 @@
import CodexBarCore
import Darwin
import Foundation
struct CodexLoginRunner {
struct Result: Equatable {
enum Outcome: Equatable {
case success
case timedOut
case failed(status: Int32)
case missingBinary
case launchFailed(String)
}
let outcome: Outcome
let output: String
}
static func run(
homePath: String? = nil,
timeout: TimeInterval = 120,
outputDrainTimeout: TimeInterval = 3,
environment: [String: String] = ProcessInfo.processInfo.environment,
loginPATH: [String]? = LoginShellPathCache.shared.current) async -> Result
{
await Task(priority: .userInitiated) {
var env = environment
env["PATH"] = PathBuilder.effectivePATH(
purposes: [.rpc, .tty, .nodeTooling],
env: env,
loginPATH: loginPATH)
env = CodexHomeScope.scopedEnvironment(base: env, codexHome: homePath)
guard let executable = BinaryLocator.resolveCodexBinary(
env: env,
loginPATH: loginPATH)
else {
return Result(outcome: .missingBinary, output: "")
}
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
process.arguments = [executable, "login"]
process.environment = env
let stdout = Pipe()
let stderr = Pipe()
process.standardOutput = stdout
process.standardError = stderr
let stdoutCapture = ProcessPipeCapture(pipe: stdout)
let stderrCapture = ProcessPipeCapture(pipe: stderr)
let termination = ProcessTermination()
process.terminationHandler = { _ in
termination.resolve(timedOut: false)
}
var processGroup: pid_t?
do {
try process.run()
processGroup = self.attachProcessGroup(process)
} catch {
return Result(outcome: .launchFailed(error.localizedDescription), output: "")
}
stdoutCapture.start()
stderrCapture.start()
let timedOut = await self.wait(timeout: timeout, termination: termination)
if timedOut {
self.terminate(process, processGroup: processGroup)
}
let output = await self.combinedOutput(
stdout: stdoutCapture,
stderr: stderrCapture,
timeout: outputDrainTimeout)
if timedOut {
return Result(outcome: .timedOut, output: output)
}
let status = process.terminationStatus
if status == 0 {
return Result(outcome: .success, output: output)
}
return Result(outcome: .failed(status: status), output: output)
}.value
}
private final class ProcessTermination: @unchecked Sendable {
private let lock = NSLock()
private var timedOut: Bool?
private var continuation: CheckedContinuation<Bool, Never>?
func resolve(timedOut: Bool) {
let continuation: CheckedContinuation<Bool, Never>?
self.lock.lock()
guard self.timedOut == nil else {
self.lock.unlock()
return
}
self.timedOut = timedOut
continuation = self.continuation
self.continuation = nil
self.lock.unlock()
continuation?.resume(returning: timedOut)
}
func wait() async -> Bool {
await withCheckedContinuation { continuation in
let timedOut: Bool?
self.lock.lock()
timedOut = self.timedOut
if timedOut == nil {
self.continuation = continuation
}
self.lock.unlock()
if let timedOut {
continuation.resume(returning: timedOut)
}
}
}
}
private static func wait(timeout: TimeInterval, termination: ProcessTermination) async -> Bool {
let timeoutTask = Task.detached(priority: .userInitiated) {
try? await Task.sleep(nanoseconds: self.timeoutNanoseconds(timeout))
if Task.isCancelled == false {
termination.resolve(timedOut: true)
}
}
let timedOut = await termination.wait()
timeoutTask.cancel()
return timedOut
}
private static func timeoutNanoseconds(_ timeout: TimeInterval) -> UInt64 {
guard timeout.isFinite else { return UInt64.max }
let seconds = max(0, min(timeout, Double(UInt64.max) / 1_000_000_000))
return UInt64(seconds * 1_000_000_000)
}
private static func terminate(_ process: Process, processGroup: pid_t?) {
if let pgid = processGroup {
kill(-pgid, SIGTERM)
}
if process.isRunning {
process.terminate()
}
let deadline = Date().addingTimeInterval(2.0)
while process.isRunning, Date() < deadline {
usleep(100_000)
}
if process.isRunning {
if let pgid = processGroup {
kill(-pgid, SIGKILL)
}
kill(process.processIdentifier, SIGKILL)
}
}
private static func attachProcessGroup(_ process: Process) -> pid_t? {
let pid = process.processIdentifier
return setpgid(pid, pid) == 0 ? pid : nil
}
private static func combinedOutput(
stdout: ProcessPipeCapture,
stderr: ProcessPipeCapture,
timeout: TimeInterval) async -> String
{
let drainTimeout = Duration.seconds(max(0, timeout))
async let outData = stdout.finish(timeout: drainTimeout)
async let errData = stderr.finish(timeout: drainTimeout)
let out = await self.decode(outData)
let err = await self.decode(errData)
let merged: String = if !out.isEmpty, !err.isEmpty {
[out, err].joined(separator: "\n")
} else {
out + err
}
let trimmed = merged.trimmingCharacters(in: .whitespacesAndNewlines)
let limited = trimmed.prefix(4000)
return limited.isEmpty ? L("No output captured.") : String(limited)
}
private static func decode(_ data: Data) -> String {
ProcessPipeCapture.decodeUTF8(data)
}
}
@@ -0,0 +1,188 @@
import CodexBarCore
import CryptoKit
import Foundation
struct CodexOwnershipContext {
let canonicalKey: String?
let canonicalEmailHashKey: String?
let historicalLegacyEmailHash: String?
let planUtilizationLegacyEmailHash: String?
let currentWeeklyResetAt: Date?
let hasAdjacentMultiAccountVeto: Bool
let hasAdjacentEmailScopeAmbiguity: Bool
}
extension UsageStore {
func codexOwnershipContext(
preferredEmail: String? = nil,
snapshot: UsageSnapshot? = nil,
includeDashboardFallback: Bool = false) -> CodexOwnershipContext
{
let resolvedIdentity = self.currentCodexRuntimeIdentity(
source: self.settings.codexResolvedActiveSource,
preferCurrentSnapshot: true,
allowLastKnownLiveFallback: true)
let activeSourceEmail = self.codexAccountScopedRefreshEmail(
preferCurrentSnapshot: true,
allowLastKnownLiveFallback: true)
let normalizedEmail = CodexIdentityResolver.normalizeEmail(
preferredEmail ??
activeSourceEmail ??
snapshot?.accountEmail(for: .codex) ??
self.snapshots[.codex]?.accountEmail(for: .codex) ??
(includeDashboardFallback ? self.codexAccountEmailForOpenAIDashboard() : nil))
let canonicalIdentity: CodexIdentity = switch resolvedIdentity {
case .unresolved:
if let normalizedEmail {
.emailOnly(normalizedEmail: normalizedEmail)
} else {
.unresolved
}
default:
resolvedIdentity
}
let legacyEmailSource: String? = switch canonicalIdentity {
case let .emailOnly(normalizedEmail):
normalizedEmail
case .providerAccount, .unresolved:
normalizedEmail
}
let attachedDashboardSnapshot = includeDashboardFallback
? self.attachedOpenAIDashboardSnapshot
: nil
let normalizedDashboardSnapshot = attachedDashboardSnapshot?
.toUsageSnapshot(provider: .codex, accountEmail: normalizedEmail)
let currentWeeklyResetAt = snapshot?.secondary?.resetsAt
?? self.snapshots[.codex]?.secondary?.resetsAt
?? normalizedDashboardSnapshot?.secondary?.resetsAt
return CodexOwnershipContext(
canonicalKey: CodexHistoryOwnership.canonicalKey(for: canonicalIdentity),
canonicalEmailHashKey: normalizedEmail.map { CodexHistoryOwnership.canonicalEmailHashKey(for: $0) },
historicalLegacyEmailHash: legacyEmailSource.map {
CodexHistoryOwnership.legacyEmailHash(normalizedEmail: $0)
},
planUtilizationLegacyEmailHash: legacyEmailSource.map {
Self.codexLegacyPlanUtilizationEmailHashKey(for: $0)
},
currentWeeklyResetAt: currentWeeklyResetAt,
hasAdjacentMultiAccountVeto: self.codexHasAdjacentMultiAccountVeto(),
hasAdjacentEmailScopeAmbiguity: normalizedEmail.map {
self.codexHasAdjacentEmailScopeAmbiguity(normalizedEmail: $0) ||
self.codexVisibleAccountsHaveAdjacentEmailScopeAmbiguity(normalizedEmail: $0)
} ?? false)
}
func codexOwnershipContext(
forVisibleAccount account: CodexVisibleAccount,
currentWeeklyResetAt: Date? = nil) -> CodexOwnershipContext
{
let normalizedEmail = CodexIdentityResolver.normalizeEmail(account.email)
let workspaceAccountID = CodexOpenAIWorkspaceResolver.normalizeWorkspaceAccountID(account.workspaceAccountID)
let canonicalIdentity: CodexIdentity = if let workspaceAccountID {
.providerAccount(id: workspaceAccountID)
} else if let normalizedEmail {
.emailOnly(normalizedEmail: normalizedEmail)
} else {
.unresolved
}
return CodexOwnershipContext(
canonicalKey: CodexHistoryOwnership.canonicalKey(for: canonicalIdentity),
canonicalEmailHashKey: normalizedEmail.map { CodexHistoryOwnership.canonicalEmailHashKey(for: $0) },
historicalLegacyEmailHash: normalizedEmail.map {
CodexHistoryOwnership.legacyEmailHash(normalizedEmail: $0)
},
planUtilizationLegacyEmailHash: normalizedEmail.map {
Self.codexLegacyPlanUtilizationEmailHashKey(for: $0)
},
currentWeeklyResetAt: currentWeeklyResetAt,
hasAdjacentMultiAccountVeto: self.codexHasAdjacentMultiAccountVeto() ||
self.codexVisibleAccountsHaveAdjacentMultiAccountVeto(),
hasAdjacentEmailScopeAmbiguity: normalizedEmail.map {
self.codexHasAdjacentEmailScopeAmbiguity(normalizedEmail: $0) ||
self.codexVisibleAccountsHaveAdjacentEmailScopeAmbiguity(normalizedEmail: $0)
} ?? false)
}
func codexHasAdjacentMultiAccountVeto() -> Bool {
let snapshot = self.settings.codexAccountReconciliationSnapshot
var distinctAccounts: Set<String> = []
if let activeManagedAccount = self.settings.activeManagedCodexAccount {
distinctAccounts.insert(CodexIdentityMatcher.selectionKey(
for: snapshot.runtimeIdentity(for: activeManagedAccount),
fallbackEmail: snapshot.runtimeEmail(for: activeManagedAccount)))
}
if let liveSystemAccount = snapshot.liveSystemAccount {
distinctAccounts.insert(CodexIdentityMatcher.selectionKey(
for: snapshot.runtimeIdentity(for: liveSystemAccount),
fallbackEmail: liveSystemAccount.email))
}
return distinctAccounts.count > 1
}
private func codexHasAdjacentEmailScopeAmbiguity(normalizedEmail: String) -> Bool {
let snapshot = self.settings.codexAccountReconciliationSnapshot
var distinctAccounts: Set<String> = []
if let activeManagedAccount = self.settings.activeManagedCodexAccount,
CodexIdentityResolver.normalizeEmail(snapshot.runtimeEmail(for: activeManagedAccount)) == normalizedEmail
{
distinctAccounts.insert(CodexIdentityMatcher.selectionKey(
for: snapshot.runtimeIdentity(for: activeManagedAccount),
fallbackEmail: snapshot.runtimeEmail(for: activeManagedAccount)))
}
if let liveSystemAccount = snapshot.liveSystemAccount,
CodexIdentityResolver.normalizeEmail(liveSystemAccount.email) == normalizedEmail
{
distinctAccounts.insert(CodexIdentityMatcher.selectionKey(
for: snapshot.runtimeIdentity(for: liveSystemAccount),
fallbackEmail: liveSystemAccount.email))
}
return distinctAccounts.count > 1
}
private func codexVisibleAccountsHaveAdjacentMultiAccountVeto() -> Bool {
let accounts = self.settings.codexVisibleAccountProjection.visibleAccounts
var distinctAccounts: Set<String> = []
for account in accounts {
if let workspaceAccountID = CodexOpenAIWorkspaceResolver.normalizeWorkspaceAccountID(
account.workspaceAccountID)
{
distinctAccounts.insert("provider:\(workspaceAccountID)")
} else if let normalizedEmail = CodexIdentityResolver.normalizeEmail(account.email) {
distinctAccounts.insert("email:\(normalizedEmail)")
}
}
return distinctAccounts.count > 1
}
private func codexVisibleAccountsHaveAdjacentEmailScopeAmbiguity(normalizedEmail: String) -> Bool {
let accounts = self.settings.codexVisibleAccountProjection.visibleAccounts
var distinctAccounts: Set<String> = []
for account in accounts where CodexIdentityResolver.normalizeEmail(account.email) == normalizedEmail {
if let workspaceAccountID = CodexOpenAIWorkspaceResolver.normalizeWorkspaceAccountID(
account.workspaceAccountID)
{
distinctAccounts.insert("provider:\(workspaceAccountID)")
} else {
distinctAccounts.insert("email:\(normalizedEmail)")
}
}
return distinctAccounts.count > 1
}
nonisolated static func codexLegacyPlanUtilizationEmailHashKey(for normalizedEmail: String) -> String {
self.sha256Hex("\(UsageProvider.codex.rawValue):email:\(normalizedEmail)")
}
nonisolated static func sha256Hex(_ input: String) -> String {
let digest = SHA256.hash(data: Data(input.utf8))
return digest.map { String(format: "%02x", $0) }.joined()
}
}
@@ -0,0 +1,63 @@
import CodexBarCore
import CryptoKit
import Foundation
@MainActor
struct CodexResetCreditExpiryNotifier {
static let expiryWindow: TimeInterval = 3 * 24 * 60 * 60
static let notificationPrefix = "codex-reset-credit-expiry"
static let summaryFingerprintsKey = "codexResetCreditExpirySummaryFingerprints"
static let maximumRememberedSummaries = 64
var userDefaults: UserDefaults = .standard
var notificationPoster: (String, String, String) -> Void = { prefix, title, body in
AppNotifications.shared.post(idPrefix: prefix, title: title, body: body)
}
func postExpiringCreditsIfNeeded(
snapshot: CodexRateLimitResetCreditsSnapshot,
resetStyle: ResetTimeDisplayStyle,
now: Date = Date())
{
let expiringCredits = snapshot.availableInventory(at: now).credits.filter { credit in
guard let expiresAt = credit.expiresAt else { return false }
return expiresAt.timeIntervalSince(now) <= Self.expiryWindow
}
guard !expiringCredits.isEmpty else { return }
let fingerprint = Self.summaryFingerprint(expiringCredits)
// Account-scoped refreshes can alternate inventories, so remember more than the latest summary.
var notifiedFingerprints = self.userDefaults.stringArray(forKey: Self.summaryFingerprintsKey) ?? []
guard !notifiedFingerprints.contains(fingerprint) else { return }
notifiedFingerprints.append(fingerprint)
if notifiedFingerprints.count > Self.maximumRememberedSummaries {
notifiedFingerprints.removeFirst(notifiedFingerprints.count - Self.maximumRememberedSummaries)
}
self.userDefaults.set(notifiedFingerprints, forKey: Self.summaryFingerprintsKey)
let expiringSnapshot = CodexRateLimitResetCreditsSnapshot(
credits: expiringCredits,
availableCount: expiringCredits.count,
updatedAt: now)
guard let presentation = CodexResetCreditsPresentation.make(
snapshot: expiringSnapshot,
resetStyle: resetStyle,
now: now)
else {
return
}
self.notificationPoster(
Self.notificationPrefix,
L("Limit Reset Credits"),
presentation.helpText)
}
private static func summaryFingerprint(_ credits: [CodexRateLimitResetCredit]) -> String {
let material = credits.map { credit in
"\(credit.id)\u{1f}\(credit.expiresAt?.timeIntervalSince1970 ?? 0)"
}.joined(separator: "\u{1e}")
return SHA256.hash(data: Data(material.utf8))
.map { String(format: "%02x", $0) }
.joined()
}
}
+596
View File
@@ -0,0 +1,596 @@
import AppKit
import CodexBarCore
import KeyboardShortcuts
import Observation
import QuartzCore
import Security
import SwiftUI
@main
struct CodexBarApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@State private var settings: SettingsStore
@State private var store: UsageStore
@State private var managedCodexAccountCoordinator: ManagedCodexAccountCoordinator
@State private var codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator
private let preferencesSelection: PreferencesSelection
private let account: AccountInfo
init() {
let env = ProcessInfo.processInfo.environment
let storedLevel = CodexBarLog.parseLevel(UserDefaults.standard.string(forKey: "debugLogLevel")) ?? .verbose
let level = CodexBarLog.parseLevel(env["CODEXBAR_LOG_LEVEL"]) ?? storedLevel
CodexBarLog.bootstrapIfNeeded(.init(
destination: .oslog(subsystem: "com.steipete.codexbar"),
level: level,
json: false))
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown"
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "unknown"
let gitCommit = Bundle.main.object(forInfoDictionaryKey: "CodexGitCommit") as? String ?? "unknown"
let buildTimestamp = Bundle.main.object(forInfoDictionaryKey: "CodexBuildTimestamp") as? String ?? "unknown"
CodexBarLog.logger(LogCategories.app).info(
"CodexBar starting",
metadata: [
"version": version,
"build": build,
"git": gitCommit,
"built": buildTimestamp,
])
KeychainAccessGate.isDisabled = UserDefaults.standard.bool(forKey: "debugDisableKeychainAccess")
KeychainPromptCoordinator.install()
if MainThreadHangWatchdog.isEnabledForCurrentProcess {
MainThreadHangWatchdog.shared.start()
}
let preferencesSelection = PreferencesSelection()
let settings = SettingsStore()
Self.applyLanguagePreference(from: settings)
configureUsageFormatterLocalizationProvider()
let managedCodexAccountCoordinator = ManagedCodexAccountCoordinator()
managedCodexAccountCoordinator.onManagedAccountsDidChange = {
_ = settings.refreshCodexAccountReconciliationAfterManagedAccountsDidChange()
}
_ = settings.persistResolvedCodexActiveSourceCorrectionIfNeeded()
let fetcher = UsageFetcher()
let browserDetection = BrowserDetection(cacheTTL: BrowserDetection.defaultCacheTTL)
let account = fetcher.loadAccountInfo()
let store = UsageStore(fetcher: fetcher, browserDetection: browserDetection, settings: settings)
let codexAccountPromotionCoordinator = CodexAccountPromotionCoordinator(
settingsStore: settings,
usageStore: store,
managedAccountCoordinator: managedCodexAccountCoordinator)
self.preferencesSelection = preferencesSelection
_settings = State(wrappedValue: settings)
_store = State(wrappedValue: store)
_managedCodexAccountCoordinator = State(wrappedValue: managedCodexAccountCoordinator)
_codexAccountPromotionCoordinator = State(wrappedValue: codexAccountPromotionCoordinator)
self.account = account
CodexBarLog.setLogLevel(settings.debugLogLevel)
self.appDelegate.configure(.init(
store: store,
settings: settings,
account: account,
selection: preferencesSelection,
managedCodexAccountCoordinator: managedCodexAccountCoordinator,
codexAccountPromotionCoordinator: codexAccountPromotionCoordinator))
}
@SceneBuilder
var body: some Scene {
// Hidden 1×1 window to keep SwiftUI's lifecycle alive so `Settings` scene
// shows the native toolbar tabs even though the UI is AppKit-based.
WindowGroup("CodexBarLifecycleKeepalive") {
HiddenWindowView()
}
.defaultSize(width: 20, height: 20)
.windowStyle(.hiddenTitleBar)
Settings {
PreferencesView(
settings: self.settings,
store: self.store,
updater: self.appDelegate.updaterController,
selection: self.preferencesSelection,
managedCodexAccountCoordinator: self.managedCodexAccountCoordinator,
codexAccountPromotionCoordinator: self.codexAccountPromotionCoordinator,
runProviderLoginFlow: { provider in
await self.appDelegate.runProviderLoginFlow(provider)
})
}
.defaultSize(width: SettingsPane.windowWidth, height: SettingsPane.windowHeight)
.windowResizability(.contentMinSize)
}
private func openSettings(pane: SettingsPane) {
self.preferencesSelection.pane = pane
NSApp.activate(ignoringOtherApps: true)
_ = NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
}
private static func applyLanguagePreference(from settings: SettingsStore) {
AppLanguagePreferenceMigration.clearLegacyOverrideIfOwned(storedAppLanguage: settings.appLanguage)
resetCodexBarLocalizationCache()
}
}
// MARK: - Updater abstraction
@MainActor
protocol UpdaterProviding: AnyObject {
var automaticallyChecksForUpdates: Bool { get set }
var automaticallyDownloadsUpdates: Bool { get set }
var isAvailable: Bool { get }
var unavailableReason: String? { get }
var updateStatus: UpdateStatus { get }
func checkForUpdates(_ sender: Any?)
func installUpdate()
}
/// No-op updater used for debug builds and non-bundled runs to suppress Sparkle dialogs.
final class DisabledUpdaterController: UpdaterProviding {
var automaticallyChecksForUpdates: Bool = false
var automaticallyDownloadsUpdates: Bool = false
let isAvailable: Bool = false
let unavailableReason: String?
let updateStatus = UpdateStatus()
init(unavailableReason: String? = nil) {
self.unavailableReason = unavailableReason
}
func checkForUpdates(_ sender: Any?) {}
func installUpdate() {}
}
@MainActor
@Observable
final class UpdateStatus {
static let disabled = UpdateStatus()
var isUpdateReady: Bool
init(isUpdateReady: Bool = false) {
self.isUpdateReady = isUpdateReady
}
}
#if canImport(Sparkle) && ENABLE_SPARKLE
import Sparkle
@MainActor
final class SparkleUpdaterController: NSObject, UpdaterProviding, SPUUpdaterDelegate {
private final class ImmediateInstallHandler: @unchecked Sendable {
private let handler: () -> Void
init(_ handler: @escaping () -> Void) {
self.handler = handler
}
func install() {
self.handler()
}
}
private lazy var controller = SPUStandardUpdaterController(
startingUpdater: false,
updaterDelegate: self,
userDriverDelegate: nil)
let updateStatus = UpdateStatus()
let unavailableReason: String? = nil
private var immediateInstallHandler: ImmediateInstallHandler?
init(savedAutoUpdate: Bool) {
super.init()
let updater = self.controller.updater
updater.automaticallyChecksForUpdates = savedAutoUpdate
updater.automaticallyDownloadsUpdates = savedAutoUpdate
self.controller.startUpdater()
}
var automaticallyChecksForUpdates: Bool {
get { self.controller.updater.automaticallyChecksForUpdates }
set { self.controller.updater.automaticallyChecksForUpdates = newValue }
}
var automaticallyDownloadsUpdates: Bool {
get { self.controller.updater.automaticallyDownloadsUpdates }
set { self.controller.updater.automaticallyDownloadsUpdates = newValue }
}
var isAvailable: Bool {
true
}
func checkForUpdates(_ sender: Any?) {
self.controller.checkForUpdates(sender)
}
func installUpdate() {
guard let immediateInstallHandler else {
self.controller.checkForUpdates(nil)
return
}
immediateInstallHandler.install()
}
nonisolated func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) {
_ = updater
_ = item
}
nonisolated func updater(_ updater: SPUUpdater, failedToDownloadUpdate item: SUAppcastItem, error: Error) {
_ = updater
_ = item
_ = error
Task { @MainActor in
self.immediateInstallHandler = nil
self.updateStatus.isUpdateReady = false
}
}
nonisolated func userDidCancelDownload(_ updater: SPUUpdater) {
_ = updater
Task { @MainActor in
self.immediateInstallHandler = nil
self.updateStatus.isUpdateReady = false
}
}
nonisolated func updater(
_ updater: SPUUpdater,
willInstallUpdateOnQuit item: SUAppcastItem,
immediateInstallationBlock immediateInstallHandler: @escaping () -> Void)
-> Bool
{
_ = updater
_ = item
let installHandler = ImmediateInstallHandler(immediateInstallHandler)
Task { @MainActor in
self.immediateInstallHandler = installHandler
self.updateStatus.isUpdateReady = true
}
return true
}
nonisolated func updater(_ updater: SPUUpdater, didAbortWithError error: Error) {
_ = updater
_ = error
Task { @MainActor in
self.immediateInstallHandler = nil
self.updateStatus.isUpdateReady = false
}
}
nonisolated func updater(
_ updater: SPUUpdater,
userDidMake choice: SPUUserUpdateChoice,
forUpdate updateItem: SUAppcastItem,
state: SPUUserUpdateState)
{
let downloaded = state.stage == .downloaded
Task { @MainActor in
switch choice {
case .install, .skip:
self.immediateInstallHandler = nil
self.updateStatus.isUpdateReady = false
case .dismiss:
self.updateStatus.isUpdateReady = downloaded
@unknown default:
self.immediateInstallHandler = nil
self.updateStatus.isUpdateReady = false
}
}
}
nonisolated func allowedChannels(for updater: SPUUpdater) -> Set<String> {
UpdateChannel.current.allowedSparkleChannels
}
}
private func isDeveloperIDSigned(bundleURL: URL) -> Bool {
var staticCode: SecStaticCode?
guard SecStaticCodeCreateWithPath(bundleURL as CFURL, SecCSFlags(), &staticCode) == errSecSuccess,
let code = staticCode else { return false }
var infoCF: CFDictionary?
guard SecCodeCopySigningInformation(code, SecCSFlags(rawValue: kSecCSSigningInformation), &infoCF) == errSecSuccess,
let info = infoCF as? [String: Any],
let certs = info[kSecCodeInfoCertificates as String] as? [SecCertificate],
let leaf = certs.first else { return false }
if let summary = SecCertificateCopySubjectSummary(leaf) as String? {
return summary.hasPrefix("Developer ID Application:")
}
return false
}
@MainActor
private func makeUpdaterController() -> UpdaterProviding {
let bundleURL = Bundle.main.bundleURL
let isBundledApp = bundleURL.pathExtension == "app"
guard isBundledApp else {
return DisabledUpdaterController(unavailableReason: "Updates unavailable in this build.")
}
if InstallOrigin.isHomebrewCask(appBundleURL: bundleURL) {
return DisabledUpdaterController(
unavailableReason: "Updates managed by Homebrew. Run: brew upgrade --cask steipete/tap/codexbar")
}
guard isDeveloperIDSigned(bundleURL: bundleURL) else {
return DisabledUpdaterController(unavailableReason: "Updates unavailable in this build.")
}
let defaults = UserDefaults.standard
let autoUpdateKey = "autoUpdateEnabled"
// Default to true for first launch; fall back to saved preference thereafter.
let savedAutoUpdate = (defaults.object(forKey: autoUpdateKey) as? Bool) ?? true
return SparkleUpdaterController(savedAutoUpdate: savedAutoUpdate)
}
#else
private func makeUpdaterController() -> UpdaterProviding {
DisabledUpdaterController()
}
#endif
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
struct Dependencies {
let store: UsageStore
let settings: SettingsStore
let account: AccountInfo
let selection: PreferencesSelection
let managedCodexAccountCoordinator: ManagedCodexAccountCoordinator
let codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator
}
let updaterController: UpdaterProviding = makeUpdaterController()
private let confettiOverlayController = ScreenConfettiOverlayController()
private let confettiLogger = CodexBarLog.logger(LogCategories.confetti)
private lazy var memoryPressureMonitor = MemoryPressureMonitor(trimAppCaches: { [weak self] in
self?.trimRebuildableCachesForMemoryPressure() ?? MemoryPressureCacheTrimSummary()
})
private var statusController: StatusItemControlling?
private var store: UsageStore?
private var settings: SettingsStore?
private var account: AccountInfo?
private var preferencesSelection: PreferencesSelection?
private var managedCodexAccountCoordinator: ManagedCodexAccountCoordinator?
private var codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator?
private var hasInstalledLimitResetObservers = false
#if DEBUG
private var debugMemoryPressureObserver: NSObjectProtocol?
#endif
var terminateActiveProcessesForAppShutdown: () -> Void = {
TTYCommandRunner.terminateActiveProcessesForAppShutdown()
}
func configure(_ dependencies: Dependencies) {
self.store = dependencies.store
self.settings = dependencies.settings
self.account = dependencies.account
self.preferencesSelection = dependencies.selection
self.managedCodexAccountCoordinator = dependencies.managedCodexAccountCoordinator
self.codexAccountPromotionCoordinator = dependencies.codexAccountPromotionCoordinator
}
func applicationWillFinishLaunching(_ notification: Notification) {
self.configureAppIconForMacOSVersion()
}
func applicationDidFinishLaunching(_ notification: Notification) {
AppNotifications.shared.requestAuthorizationOnStartup()
self.memoryPressureMonitor.start()
#if DEBUG
self.installDebugMemoryPressureObserverIfNeeded()
#endif
self.ensureStatusController()
KeyboardShortcuts.onKeyUp(for: .openMenu) { [weak self] in
// KeyboardShortcuts dispatches both normal and menu-tracking hotkeys on the main event loop.
MainActor.assumeIsolated {
self?.statusController?.openMenuFromShortcut()
}
}
if !self.hasInstalledLimitResetObservers {
NotificationCenter.default.addObserver(
self,
selector: #selector(self.handleSessionLimitResetNotification(_:)),
name: .codexbarSessionLimitReset,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.handleWeeklyLimitResetNotification(_:)),
name: .codexbarWeeklyLimitReset,
object: nil)
self.hasInstalledLimitResetObservers = true
}
}
func applicationWillTerminate(_ notification: Notification) {
self.memoryPressureMonitor.stop()
#if DEBUG
self.removeDebugMemoryPressureObserver()
#endif
self.statusController?.prepareForAppShutdown()
self.confettiOverlayController.dismiss()
self.dismissAppKitWindowsForShutdown()
self.terminateActiveProcessesForAppShutdown()
}
func runProviderLoginFlow(_ provider: UsageProvider) async {
self.ensureStatusController()
guard let statusController else { return }
await statusController.runLoginFlowFromSettings(provider: provider)
}
@objc private func handleSessionLimitResetNotification(_ notification: Notification) {
guard let event = notification.object as? SessionLimitResetEvent else { return }
guard self.settings?.confettiOnSessionLimitResetsEnabled == true else { return }
self.playLimitResetConfetti(
provider: event.provider,
accountIdentifier: event.accountIdentifier,
resetKind: "session")
}
@objc private func handleWeeklyLimitResetNotification(_ notification: Notification) {
guard let event = notification.object as? WeeklyLimitResetEvent else { return }
guard self.settings?.confettiOnWeeklyLimitResetsEnabled == true else { return }
self.playLimitResetConfetti(
provider: event.provider,
accountIdentifier: event.accountIdentifier,
resetKind: "weekly")
}
private func playLimitResetConfetti(
provider: UsageProvider,
accountIdentifier: String,
resetKind: String)
{
let origin = self.statusController?.celebrationOriginPoint(for: provider)
self.confettiLogger.info(
"Triggering confetti",
metadata: [
"provider": provider.rawValue,
"accountIdentifier": accountIdentifier,
"resetKind": resetKind,
"originKnown": origin == nil ? "0" : "1",
])
self.confettiOverlayController.play(originInScreen: origin)
}
/// Use the classic (non-Liquid Glass) app icon on macOS versions before 26.
private func configureAppIconForMacOSVersion() {
if #unavailable(macOS 26) {
self.applyClassicAppIcon()
}
}
private func applyClassicAppIcon() {
guard let classicIcon = Self.loadClassicIcon() else { return }
NSApp.applicationIconImage = classicIcon
}
private static func loadClassicIcon() -> NSImage? {
guard let url = self.classicIconURL(),
let image = NSImage(contentsOf: url)
else {
return nil
}
return image
}
private static func classicIconURL() -> URL? {
Bundle.main.url(forResource: "Icon-classic", withExtension: "icns")
}
private func dismissAppKitWindowsForShutdown() {
guard let app = NSApp else { return }
for window in app.windows {
window.orderOut(nil)
}
}
private func ensureStatusController() {
if self.statusController != nil { return }
if let store,
let settings,
let account,
let selection = self.preferencesSelection,
let managedCodexAccountCoordinator,
let codexAccountPromotionCoordinator
{
self.statusController = StatusItemController.factory(
store,
settings,
account,
self.updaterController,
selection,
managedCodexAccountCoordinator,
codexAccountPromotionCoordinator)
return
}
// Defensive fallback: this should not be hit in normal app lifecycle.
CodexBarLog.logger(LogCategories.app)
.error("StatusItemController fallback path used; settings/store mismatch likely.")
assertionFailure("StatusItemController fallback path used; check app lifecycle wiring.")
let fallbackSettings = SettingsStore()
let fetcher = UsageFetcher()
let browserDetection = BrowserDetection(cacheTTL: BrowserDetection.defaultCacheTTL)
let fallbackAccount = fetcher.loadAccountInfo()
let fallbackStore = UsageStore(fetcher: fetcher, browserDetection: browserDetection, settings: fallbackSettings)
let fallbackManagedCodexAccountCoordinator = ManagedCodexAccountCoordinator()
let fallbackCodexAccountPromotionCoordinator = CodexAccountPromotionCoordinator(
settingsStore: fallbackSettings,
usageStore: fallbackStore,
managedAccountCoordinator: fallbackManagedCodexAccountCoordinator)
self.statusController = StatusItemController.factory(
fallbackStore,
fallbackSettings,
fallbackAccount,
self.updaterController,
PreferencesSelection(),
fallbackManagedCodexAccountCoordinator,
fallbackCodexAccountPromotionCoordinator)
}
private func trimRebuildableCachesForMemoryPressure() -> MemoryPressureCacheTrimSummary {
var summary = MemoryPressureCacheTrimSummary()
let statusSummary = self.statusController?.trimRebuildableCachesForMemoryPressure()
?? MemoryPressureCacheTrimSummary()
let storeSummary = self.store?.trimRebuildableCachesForMemoryPressure()
?? MemoryPressureCacheTrimSummary()
summary.merge(statusSummary)
summary.merge(storeSummary)
return summary
}
#if DEBUG
private func installDebugMemoryPressureObserverIfNeeded() {
guard self.debugMemoryPressureObserver == nil else { return }
self.debugMemoryPressureObserver = DistributedNotificationCenter.default().addObserver(
forName: .codexbarDebugSimulateMemoryPressure,
object: nil,
queue: .main)
{ [weak self] notification in
let rawLevel = notification.userInfo?["level"] as? String
let shouldSeedCaches = notification.userInfo?["seedCaches"] as? String == "1"
MainActor.assumeIsolated {
self?.handleDebugMemoryPressureNotification(
rawLevel: rawLevel,
shouldSeedCaches: shouldSeedCaches)
}
}
}
private func removeDebugMemoryPressureObserver() {
guard let observer = self.debugMemoryPressureObserver else { return }
DistributedNotificationCenter.default().removeObserver(observer)
self.debugMemoryPressureObserver = nil
}
private func handleDebugMemoryPressureNotification(rawLevel: String?, shouldSeedCaches: Bool) {
let isCritical = rawLevel?.caseInsensitiveCompare("critical") == .orderedSame
if shouldSeedCaches {
OpenAIDashboardFetcher.seedCachedWebViewsForMemoryPressureProof()
self.statusController?.seedRebuildableCachesForMemoryPressureProof()
self.store?.seedRebuildableCachesForMemoryPressureProof()
}
CodexBarLog.logger(LogCategories.memoryPressure).info(
"Debug memory pressure notification received",
metadata: [
"level": isCritical ? "critical" : "warning",
"seedCaches": shouldSeedCaches ? "1" : "0",
])
self.memoryPressureMonitor.handleMemoryPressureForTesting(isWarning: !isCritical, isCritical: isCritical)
}
#endif
deinit {
NotificationCenter.default.removeObserver(self)
}
}
@@ -0,0 +1,373 @@
import CodexBarCore
import Foundation
struct CodexBarConfigMigrator {
struct LegacyStores {
let zaiTokenStore: any ZaiTokenStoring
let syntheticTokenStore: any SyntheticTokenStoring
let codexCookieStore: any CookieHeaderStoring
let claudeCookieStore: any CookieHeaderStoring
let cursorCookieStore: any CookieHeaderStoring
let opencodeCookieStore: any CookieHeaderStoring
let factoryCookieStore: any CookieHeaderStoring
let minimaxCookieStore: any MiniMaxCookieStoring
let minimaxAPITokenStore: any MiniMaxAPITokenStoring
let kimiTokenStore: any KimiTokenStoring
let kimiK2TokenStore: any KimiK2TokenStoring
let augmentCookieStore: any CookieHeaderStoring
let ampCookieStore: any CookieHeaderStoring
let copilotTokenStore: any CopilotTokenStoring
let tokenAccountStore: any ProviderTokenAccountStoring
}
private static let legacyMigrationCompletedKey = "codexbar.legacySecretsMigrationCompleted"
private struct MigrationState {
var didUpdate = false
var sawLegacySecrets = false
var sawLegacyAccounts = false
}
static func loadOrMigrate(
configStore: CodexBarConfigStore,
userDefaults: UserDefaults,
stores: LegacyStores) -> CodexBarConfig
{
let log = CodexBarLog.logger(LogCategories.configMigration)
let existing = try? configStore.load()
var config = (existing ?? CodexBarConfig.makeDefault()).normalized()
var state = MigrationState()
// applyLegacyCookieSources reads only UserDefaults cheap, runs unconditionally so
// newly-added cookie-source keys are picked up on every launch.
self.applyLegacyCookieSources(userDefaults: userDefaults, config: &config, state: &state)
let migrationCompleted = userDefaults.bool(forKey: Self.legacyMigrationCompletedKey)
if !migrationCompleted {
// Run once: migrate Keychain/file secrets then clear them. Using a completion flag rather
// than `existing == nil` ensures a crash between config-save and clearLegacyStores can
// finish cleanup on the next launch without re-doing the (already-saved) data migration.
if existing == nil {
self.applyLegacyOrderAndToggles(userDefaults: userDefaults, config: &config, state: &state)
}
self.migrateLegacySecrets(userDefaults: userDefaults, stores: stores, config: &config, state: &state)
self.migrateLegacyAccounts(stores: stores, config: &config, state: &state)
}
var didPersistUpdates = true
if state.didUpdate {
do {
try configStore.save(config)
} catch {
didPersistUpdates = false
log.error("Failed to persist config: \(error)")
}
}
guard didPersistUpdates else {
return config.normalized()
}
if state.sawLegacySecrets || state.sawLegacyAccounts {
let cleared = self.clearLegacyStores(stores: stores, sawAccounts: state.sawLegacyAccounts, log: log)
if cleared {
userDefaults.set(true, forKey: Self.legacyMigrationCompletedKey)
}
} else if !migrationCompleted {
userDefaults.set(true, forKey: Self.legacyMigrationCompletedKey)
}
return config.normalized()
}
private static func applyLegacyOrderAndToggles(
userDefaults: UserDefaults,
config: inout CodexBarConfig,
state: inout MigrationState)
{
if let order = userDefaults.stringArray(forKey: "providerOrder"), !order.isEmpty {
config = self.applyProviderOrder(order, config: config)
state.didUpdate = true
}
let toggles = userDefaults.dictionary(forKey: "providerToggles") as? [String: Bool] ?? [:]
if !toggles.isEmpty {
config = self.applyProviderToggles(toggles, config: config)
state.didUpdate = true
}
}
private static func migrateLegacySecrets(
userDefaults: UserDefaults,
stores: LegacyStores,
config: inout CodexBarConfig,
state: inout MigrationState)
{
self.migrateTokenProviders(
[
(.zai, stores.zaiTokenStore.loadToken),
(.synthetic, stores.syntheticTokenStore.loadToken),
(.copilot, stores.copilotTokenStore.loadToken),
(.kimik2, stores.kimiK2TokenStore.loadToken),
],
config: &config,
state: &state)
self.migrateCookieProviders(
[
(.codex, stores.codexCookieStore.loadCookieHeader),
(.claude, stores.claudeCookieStore.loadCookieHeader),
(.cursor, stores.cursorCookieStore.loadCookieHeader),
(.factory, stores.factoryCookieStore.loadCookieHeader),
(.augment, stores.augmentCookieStore.loadCookieHeader),
(.amp, stores.ampCookieStore.loadCookieHeader),
],
config: &config,
state: &state)
self.migrateMiniMax(userDefaults: userDefaults, stores: stores, config: &config, state: &state)
self.migrateKimi(userDefaults: userDefaults, stores: stores, config: &config, state: &state)
self.migrateOpenCode(userDefaults: userDefaults, stores: stores, config: &config, state: &state)
}
private static func applyLegacyCookieSources(
userDefaults: UserDefaults,
config: inout CodexBarConfig,
state: inout MigrationState)
{
let sources: [(UsageProvider, String)] = [
(.codex, "codexCookieSource"),
(.claude, "claudeCookieSource"),
(.cursor, "cursorCookieSource"),
(.opencode, "opencodeCookieSource"),
(.factory, "factoryCookieSource"),
(.minimax, "minimaxCookieSource"),
(.kimi, "kimiCookieSource"),
(.augment, "augmentCookieSource"),
(.amp, "ampCookieSource"),
]
for (provider, key) in sources {
guard let raw = userDefaults.string(forKey: key),
let source = ProviderCookieSource(rawValue: raw)
else { continue }
self.updateProvider(provider, config: &config, state: &state) { entry in
guard entry.cookieSource == nil else { return false }
entry.cookieSource = source
return true
}
}
if userDefaults.object(forKey: "openAIWebAccessEnabled") as? Bool == false {
self.updateProvider(.codex, config: &config, state: &state) { entry in
guard entry.cookieSource == nil else { return false }
entry.cookieSource = .off
return true
}
}
}
private static func migrateTokenProviders(
_ providers: [(UsageProvider, () throws -> String?)],
config: inout CodexBarConfig,
state: inout MigrationState)
{
for (provider, loader) in providers {
let token = try? loader()
if token != nil { state.sawLegacySecrets = true }
self.updateProvider(provider, config: &config, state: &state) { entry in
self.setIfEmpty(&entry.apiKey, token)
}
}
}
private static func migrateCookieProviders(
_ providers: [(UsageProvider, () throws -> String?)],
config: inout CodexBarConfig,
state: inout MigrationState)
{
for (provider, loader) in providers {
let header = try? loader()
if header != nil { state.sawLegacySecrets = true }
self.updateProvider(provider, config: &config, state: &state) { entry in
self.setIfEmpty(&entry.cookieHeader, header)
}
}
}
private static func migrateMiniMax(
userDefaults: UserDefaults,
stores: LegacyStores,
config: inout CodexBarConfig,
state: inout MigrationState)
{
let token = try? stores.minimaxAPITokenStore.loadToken()
let header = try? stores.minimaxCookieStore.loadCookieHeader()
if token != nil || header != nil {
state.sawLegacySecrets = true
}
let regionRaw = userDefaults.string(forKey: "minimaxAPIRegion")
self.updateProvider(.minimax, config: &config, state: &state) { entry in
var changed = false
changed = self.setIfEmpty(&entry.apiKey, token) || changed
if let regionRaw, !regionRaw.isEmpty, entry.region == nil {
entry.region = regionRaw
changed = true
}
changed = self.setIfEmpty(&entry.cookieHeader, header) || changed
return changed
}
}
private static func migrateKimi(
userDefaults: UserDefaults,
stores: LegacyStores,
config: inout CodexBarConfig,
state: inout MigrationState)
{
var token = try? stores.kimiTokenStore.loadToken()
if token?.isEmpty ?? true {
token = userDefaults.string(forKey: "kimiManualCookieHeader")
}
if token != nil { state.sawLegacySecrets = true }
self.updateProvider(.kimi, config: &config, state: &state) { entry in
self.setIfEmpty(&entry.cookieHeader, token)
}
}
private static func migrateOpenCode(
userDefaults: UserDefaults,
stores: LegacyStores,
config: inout CodexBarConfig,
state: inout MigrationState)
{
let header = try? stores.opencodeCookieStore.loadCookieHeader()
if header != nil { state.sawLegacySecrets = true }
let workspaceID = userDefaults.string(forKey: "opencodeWorkspaceID")
self.updateProvider(.opencode, config: &config, state: &state) { entry in
var changed = false
changed = self.setIfEmpty(&entry.cookieHeader, header) || changed
if let workspaceID, !workspaceID.isEmpty, entry.workspaceID == nil {
entry.workspaceID = workspaceID
changed = true
}
return changed
}
}
private static func migrateLegacyAccounts(
stores: LegacyStores,
config: inout CodexBarConfig,
state: inout MigrationState)
{
guard let accounts = try? stores.tokenAccountStore.loadAccounts(), !accounts.isEmpty else { return }
state.sawLegacyAccounts = true
for (provider, data) in accounts where !data.accounts.isEmpty {
self.updateProvider(provider, config: &config, state: &state) { entry in
guard entry.tokenAccounts == nil else { return false }
entry.tokenAccounts = data
return true
}
}
}
private static func updateProvider(
_ provider: UsageProvider,
config: inout CodexBarConfig,
state: inout MigrationState,
mutate: (inout ProviderConfig) -> Bool)
{
guard let index = config.providers.firstIndex(where: { $0.id == provider }) else { return }
var entry = config.providers[index]
let changed = mutate(&entry)
if changed {
config.providers[index] = entry
state.didUpdate = true
}
}
private static func setIfEmpty(_ value: inout String?, _ replacement: String?) -> Bool {
let cleaned = replacement?.trimmingCharacters(in: .whitespacesAndNewlines)
guard let cleaned, !cleaned.isEmpty else { return false }
if value == nil || value?.isEmpty == true {
value = cleaned
return true
}
return false
}
@discardableResult
private static func clearLegacyStores(
stores: LegacyStores,
sawAccounts: Bool,
log: CodexBarLogger) -> Bool
{
var success = true
do {
try stores.zaiTokenStore.storeToken(nil)
try stores.syntheticTokenStore.storeToken(nil)
try stores.copilotTokenStore.storeToken(nil)
try stores.minimaxAPITokenStore.storeToken(nil)
try stores.kimiTokenStore.storeToken(nil)
try stores.kimiK2TokenStore.storeToken(nil)
try stores.codexCookieStore.storeCookieHeader(nil)
try stores.claudeCookieStore.storeCookieHeader(nil)
try stores.cursorCookieStore.storeCookieHeader(nil)
try stores.opencodeCookieStore.storeCookieHeader(nil)
try stores.factoryCookieStore.storeCookieHeader(nil)
try stores.minimaxCookieStore.storeCookieHeader(nil)
try stores.augmentCookieStore.storeCookieHeader(nil)
try stores.ampCookieStore.storeCookieHeader(nil)
} catch {
log.error("Failed to clear legacy secrets: \(error)")
success = false
}
if sawAccounts {
let legacyURL = FileTokenAccountStore.defaultURL()
if FileManager.default.fileExists(atPath: legacyURL.path) {
try? FileManager.default.removeItem(at: legacyURL)
}
}
return success
}
private static func applyProviderOrder(_ raw: [String], config: CodexBarConfig) -> CodexBarConfig {
let configsByID = Dictionary(uniqueKeysWithValues: config.providers.map { ($0.id, $0) })
var seen: Set<UsageProvider> = []
var ordered: [ProviderConfig] = []
ordered.reserveCapacity(config.providers.count)
for rawValue in raw {
guard let provider = UsageProvider(rawValue: rawValue),
let entry = configsByID[provider],
!seen.contains(provider)
else { continue }
seen.insert(provider)
ordered.append(entry)
}
for provider in UsageProvider.allCases where !seen.contains(provider) {
ordered.append(configsByID[provider] ?? ProviderConfig(id: provider))
}
var updated = config
updated.providers = ordered
return updated
}
private static func applyProviderToggles(
_ toggles: [String: Bool],
config: CodexBarConfig) -> CodexBarConfig
{
var updated = config
for index in updated.providers.indices {
let provider = updated.providers[index].id
let meta = ProviderDescriptorRegistry.descriptor(for: provider).metadata
if let value = toggles[meta.cliName] {
updated.providers[index].enabled = value
}
}
return updated
}
}
+190
View File
@@ -0,0 +1,190 @@
import CodexBarCore
import Foundation
import Security
protocol CookieHeaderStoring: Sendable {
func loadCookieHeader() throws -> String?
func storeCookieHeader(_ header: String?) throws
}
enum CookieHeaderStoreError: LocalizedError {
case keychainStatus(OSStatus)
case invalidData
var errorDescription: String? {
switch self {
case let .keychainStatus(status):
"Keychain error: \(status)"
case .invalidData:
"Keychain returned invalid data."
}
}
}
struct KeychainCookieHeaderStore: CookieHeaderStoring {
private static let log = CodexBarLog.logger(LogCategories.cookieHeaderStore)
private let service = "com.steipete.CodexBar"
private let account: String
private let promptKind: KeychainPromptContext.Kind
// Cache to reduce keychain access frequency
private nonisolated(unsafe) static var cache: [String: CachedValue] = [:]
private static let cacheLock = NSLock()
private static let cacheTTL: TimeInterval = 1800 // 30 minutes
private struct CachedValue {
let value: String?
let timestamp: Date
var isExpired: Bool {
Date().timeIntervalSince(self.timestamp) > KeychainCookieHeaderStore.cacheTTL
}
}
init(account: String, promptKind: KeychainPromptContext.Kind) {
self.account = account
self.promptKind = promptKind
}
func loadCookieHeader() throws -> String? {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping cookie load")
return nil
}
// Check cache first
Self.cacheLock.lock()
if let cached = Self.cache[self.account], !cached.isExpired {
Self.cacheLock.unlock()
Self.log.debug("Using cached cookie header for \(self.account)")
return cached.value
}
Self.cacheLock.unlock()
var result: CFTypeRef?
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true,
]
if case .interactionRequired = KeychainAccessPreflight
.checkGenericPassword(service: self.service, account: self.account)
{
KeychainPromptHandler.handler?(KeychainPromptContext(
kind: self.promptKind,
service: self.service,
account: self.account))
}
let status = KeychainSecurity.copyMatching(query as CFDictionary, &result)
if status == errSecItemNotFound {
// Cache the nil result
Self.cacheLock.lock()
Self.cache[self.account] = CachedValue(value: nil, timestamp: Date())
Self.cacheLock.unlock()
return nil
}
guard status == errSecSuccess else {
Self.log.error("Keychain read failed: \(status)")
throw CookieHeaderStoreError.keychainStatus(status)
}
guard let data = result as? Data else {
throw CookieHeaderStoreError.invalidData
}
let header = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
let finalValue = (header?.isEmpty == false) ? header : nil
// Cache the result
Self.cacheLock.lock()
Self.cache[self.account] = CachedValue(value: finalValue, timestamp: Date())
Self.cacheLock.unlock()
return finalValue
}
func storeCookieHeader(_ header: String?) throws {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping cookie store")
return
}
guard let raw = header?.trimmingCharacters(in: .whitespacesAndNewlines),
!raw.isEmpty
else {
try self.deleteIfPresent()
// Invalidate cache
Self.cacheLock.lock()
Self.cache.removeValue(forKey: self.account)
Self.cacheLock.unlock()
return
}
guard CookieHeaderNormalizer.normalize(raw) != nil else {
try self.deleteIfPresent()
// Invalidate cache
Self.cacheLock.lock()
Self.cache.removeValue(forKey: self.account)
Self.cacheLock.unlock()
return
}
let data = raw.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let attributes: [String: Any] = [
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
]
let updateStatus = KeychainSecurity.update(query as CFDictionary, attributes as CFDictionary)
if updateStatus == errSecSuccess {
// Update cache
Self.cacheLock.lock()
Self.cache[self.account] = CachedValue(value: raw, timestamp: Date())
Self.cacheLock.unlock()
return
}
if updateStatus != errSecItemNotFound {
Self.log.error("Keychain update failed: \(updateStatus)")
throw CookieHeaderStoreError.keychainStatus(updateStatus)
}
var addQuery = query
for (key, value) in attributes {
addQuery[key] = value
}
let addStatus = KeychainSecurity.add(addQuery as CFDictionary, nil)
guard addStatus == errSecSuccess else {
Self.log.error("Keychain add failed: \(addStatus)")
throw CookieHeaderStoreError.keychainStatus(addStatus)
}
// Update cache
Self.cacheLock.lock()
Self.cache[self.account] = CachedValue(value: raw, timestamp: Date())
Self.cacheLock.unlock()
}
private func deleteIfPresent() throws {
guard !KeychainAccessGate.isDisabled else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let status = KeychainSecurity.delete(query as CFDictionary)
if status == errSecSuccess || status == errSecItemNotFound {
// Invalidate cache
Self.cacheLock.lock()
Self.cache.removeValue(forKey: self.account)
Self.cacheLock.unlock()
return
}
Self.log.error("Keychain delete failed: \(status)")
throw CookieHeaderStoreError.keychainStatus(status)
}
}
+128
View File
@@ -0,0 +1,128 @@
import CodexBarCore
import Foundation
import Security
protocol CopilotTokenStoring: Sendable {
func loadToken() throws -> String?
func storeToken(_ token: String?) throws
}
enum CopilotTokenStoreError: LocalizedError {
case keychainStatus(OSStatus)
case invalidData
var errorDescription: String? {
switch self {
case let .keychainStatus(status):
"Keychain error: \(status)"
case .invalidData:
"Keychain returned invalid data."
}
}
}
struct KeychainCopilotTokenStore: CopilotTokenStoring {
private static let log = CodexBarLog.logger(LogCategories.copilotTokenStore)
private let service = "com.steipete.CodexBar"
private let account = "copilot-api-token"
func loadToken() throws -> String? {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping token load")
return nil
}
var result: CFTypeRef?
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true,
]
if case .interactionRequired = KeychainAccessPreflight
.checkGenericPassword(service: self.service, account: self.account)
{
KeychainPromptHandler.handler?(KeychainPromptContext(
kind: .copilotToken,
service: self.service,
account: self.account))
}
let status = KeychainSecurity.copyMatching(query as CFDictionary, &result)
if status == errSecItemNotFound {
return nil
}
guard status == errSecSuccess else {
Self.log.error("Keychain read failed: \(status)")
throw CopilotTokenStoreError.keychainStatus(status)
}
guard let data = result as? Data else {
throw CopilotTokenStoreError.invalidData
}
let token = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
if let token, !token.isEmpty {
return token
}
return nil
}
func storeToken(_ token: String?) throws {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping token store")
return
}
let cleaned = token?.trimmingCharacters(in: .whitespacesAndNewlines)
if cleaned == nil || cleaned?.isEmpty == true {
try self.deleteTokenIfPresent()
return
}
let data = cleaned!.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let attributes: [String: Any] = [
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
]
let updateStatus = KeychainSecurity.update(query as CFDictionary, attributes as CFDictionary)
if updateStatus == errSecSuccess {
return
}
if updateStatus != errSecItemNotFound {
Self.log.error("Keychain update failed: \(updateStatus)")
throw CopilotTokenStoreError.keychainStatus(updateStatus)
}
var addQuery = query
for (key, value) in attributes {
addQuery[key] = value
}
let addStatus = KeychainSecurity.add(addQuery as CFDictionary, nil)
guard addStatus == errSecSuccess else {
Self.log.error("Keychain add failed: \(addStatus)")
throw CopilotTokenStoreError.keychainStatus(addStatus)
}
}
private func deleteTokenIfPresent() throws {
guard !KeychainAccessGate.isDisabled else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let status = KeychainSecurity.delete(query as CFDictionary)
if status == errSecSuccess || status == errSecItemNotFound {
return
}
Self.log.error("Keychain delete failed: \(status)")
throw CopilotTokenStoreError.keychainStatus(status)
}
}
@@ -0,0 +1,909 @@
import Charts
import CodexBarCore
import SwiftUI
@MainActor
struct CostHistoryChartMenuView: View {
typealias DailyEntry = CostUsageDailyReport.Entry
enum AxisLabelPlacement: Equatable {
case hidden
case centered
case edges
}
private struct Point: Identifiable {
let id: String
let date: Date
let costUSD: Double
let totalTokens: Int?
let requestCount: Int?
init(date: Date, costUSD: Double, totalTokens: Int?, requestCount: Int?) {
self.date = date
self.costUSD = costUSD
self.totalTokens = totalTokens
self.requestCount = requestCount
self.id = "\(Int(date.timeIntervalSince1970))-\(costUSD)"
}
}
private struct DetailRow: Identifiable {
let id: String
let title: String
let subtitle: String?
let modeSubtitle: String?
let accentColor: Color
}
private struct DetailContent {
let primary: String
let rows: [DetailRow]
}
private let provider: UsageProvider
private let daily: [DailyEntry]
private let totalCostUSD: Double?
private let currencyCode: String
private let historyDays: Int
private let windowLabel: String?
private let projects: [CostUsageProjectBreakdown]
private let width: CGFloat
@State private var selectedDateKey: String?
init(
provider: UsageProvider,
daily: [DailyEntry],
totalCostUSD: Double?,
currencyCode: String = "USD",
historyDays: Int = 30,
windowLabel: String? = nil,
projects: [CostUsageProjectBreakdown] = [],
width: CGFloat)
{
self.provider = provider
self.daily = daily
self.totalCostUSD = totalCostUSD
self.currencyCode = currencyCode
self.historyDays = max(1, min(365, historyDays))
self.windowLabel = windowLabel
self.projects = projects
self.width = width
}
var body: some View {
let model = Self.makeModel(provider: self.provider, daily: self.daily)
let selectedDateKey = self.selectedDateKey ?? Self.defaultSelectedDateKey(model: model)
VStack(alignment: .leading, spacing: Self.outerSpacing) {
if model.points.isEmpty {
Text(L("No cost history data."))
.font(.footnote)
.foregroundStyle(.secondary)
.accessibilityLabel(L("No cost history data."))
} else {
Chart {
ForEach(model.points) { point in
BarMark(
x: .value(L("Day"), point.date, unit: .day),
y: .value(L("Cost"), point.costUSD))
.foregroundStyle(model.barColor)
}
if let peak = Self.peakPoint(model: model) {
let capStart = max(peak.costUSD - Self.capHeight(maxValue: model.maxCostUSD), 0)
BarMark(
x: .value(L("Day"), peak.date, unit: .day),
yStart: .value(L("Cap start"), capStart),
yEnd: .value(L("Cap end"), peak.costUSD))
.foregroundStyle(Color(nsColor: .systemYellow))
}
}
.chartYAxis {
AxisMarks(position: .leading, values: Self.yAxisTickValues(maxCostUSD: model.maxCostUSD)) { value in
AxisGridLine().foregroundStyle(Color.clear)
AxisTick().foregroundStyle(Color.clear)
AxisValueLabel(centered: false) {
if let raw = value.as(Double.self) {
Text(Self.yAxisCostString(raw, currencyCode: self.currencyCode))
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.padding(.leading, 4)
}
}
}
}
.chartXAxis {
AxisMarks(values: model.axisDates) { value in
AxisGridLine().foregroundStyle(Color.clear)
AxisTick().foregroundStyle(Color.clear)
if let date = value.as(Date.self) {
AxisValueLabel(anchor: Self.xAxisLabelAnchor(for: date, axisDates: model.axisDates)) {
Text(date, format: .dateTime.month(.abbreviated).day())
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
}
}
}
}
.chartLegend(.hidden)
.frame(height: Self.chartHeight)
.accessibilityLabel(L("Cost history chart"))
.accessibilityValue(
model.points.isEmpty
? L("No data")
: String(format: L("%d days of cost data"), model.points.count))
.chartOverlay { proxy in
GeometryReader { geo in
ZStack(alignment: .topLeading) {
if let rect = self.selectionBandRect(model: model, proxy: proxy, geo: geo) {
Rectangle()
.fill(Self.selectionBandColor)
.frame(width: rect.width, height: rect.height)
.position(x: rect.midX, y: rect.midY)
.allowsHitTesting(false)
}
MouseLocationReader { location in
self.updateSelection(location: location, model: model, proxy: proxy, geo: geo)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
}
}
}
let detail = self.detailContent(selectedDateKey: selectedDateKey, model: model)
VStack(alignment: .leading, spacing: Self.detailSpacing) {
Text(detail.primary)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.tail)
.frame(height: Self.detailPrimaryLineHeight, alignment: .leading)
if model.detailViewportRowCount > 0 {
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: Self.detailSpacing) {
ForEach(detail.rows) { row in
HStack(alignment: .top, spacing: 8) {
Rectangle()
.fill(row.accentColor)
.frame(
width: 2,
height: Self.accentHeight(
for: row,
rowHeight: model.detailRowHeight))
.padding(.top, 1)
VStack(alignment: .leading, spacing: 1) {
Text(row.title)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.tail)
.frame(height: Self.detailTitleLineHeight, alignment: .leading)
if let subtitle = row.subtitle {
Text(subtitle)
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineLimit(1)
.truncationMode(.tail)
.frame(
height: Self.detailSubtitleLineHeight,
alignment: .leading)
}
if let modeSubtitle = row.modeSubtitle {
Text(modeSubtitle)
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineLimit(1)
.truncationMode(.tail)
.frame(
height: Self.detailSubtitleLineHeight,
alignment: .leading)
}
}
}
.frame(height: model.detailRowHeight, alignment: .leading)
}
}
}
.scrollIndicators(
Self.detailRowsNeedScrolling(itemCount: detail.rows.count) ? .visible : .hidden)
.frame(
height: Self.detailRowsViewportHeight(
rowCount: model.detailViewportRowCount,
rowHeight: model.detailRowHeight),
alignment: .topLeading)
.id(selectedDateKey)
if model.hasDetailOverflow {
Text(Self.detailOverflowHint(itemCount: detail.rows.count) ?? " ")
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.frame(height: Self.detailHintHeight, alignment: .leading)
.accessibilityHidden(!Self.detailRowsNeedScrolling(itemCount: detail.rows.count))
}
}
}
.frame(
height: Self.detailBlockHeight(
rowCount: model.detailViewportRowCount,
hasOverflow: model.hasDetailOverflow,
rowHeight: model.detailRowHeight),
alignment: .topLeading)
}
if let total = self.totalCostUSD {
Text(String(
format: L("Est. total (%@): %@"),
self.windowLabel ?? Self.windowLabel(days: self.historyDays),
self.costString(total)))
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.head)
.frame(height: Self.detailPrimaryLineHeight, alignment: .leading)
}
if !self.projects.isEmpty {
VStack(alignment: .leading, spacing: Self.projectRowSpacing) {
Text("Projects")
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.frame(height: Self.detailPrimaryLineHeight, alignment: .leading)
ForEach(Array(self.projects.prefix(Self.maxVisibleProjectRows)), id: \.projectRowID) { project in
let visibleSources = Self.visibleProjectSources(project)
VStack(alignment: .leading, spacing: Self.projectSourceSpacing) {
self.projectParentRow(project)
if !visibleSources.isEmpty {
ForEach(
Array(visibleSources.prefix(Self.maxVisibleProjectSourceRows)),
id: \.sourceRowID)
{ source in
self.projectSourceRow(source)
}
let hiddenSourceCount = visibleSources.count - Self.maxVisibleProjectSourceRows
if hiddenSourceCount > 0 {
Text("+ \(hiddenSourceCount) more")
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineLimit(1)
.padding(.leading, Self.projectSourceIndent)
.frame(height: Self.projectMoreRowHeight, alignment: .leading)
}
}
}
.frame(height: Self.projectEntryHeight(project), alignment: .topLeading)
}
}
.frame(height: Self.projectBlockHeight(projects: self.projects), alignment: .topLeading)
}
}
.padding(.horizontal, 16)
.padding(.vertical, Self.verticalPadding)
.frame(minWidth: self.width, maxWidth: .infinity, alignment: .top)
}
private struct Model {
let points: [Point]
let pointsByDateKey: [String: Point]
let entriesByDateKey: [String: DailyEntry]
let dateKeys: [(key: String, date: Date)]
let axisDates: [Date]
let barColor: Color
let peakKey: String?
let maxCostUSD: Double
let detailViewportRowCount: Int
let hasDetailOverflow: Bool
let detailRowHeight: CGFloat
}
private static let selectionBandColor = Color(nsColor: .labelColor).opacity(0.1)
static let maxVisibleDetailLines = 4
private static let detailPrimaryLineHeight: CGFloat = 16
private static let detailTitleLineHeight: CGFloat = 16
private static let detailSubtitleLineHeight: CGFloat = 13
private static let compactDetailRowHeight: CGFloat = 36
private static let expandedDetailRowHeight: CGFloat = 44
private static let detailSpacing: CGFloat = 6
private static let detailHintHeight: CGFloat = 13
private static let chartHeight: CGFloat = 130
private static let outerSpacing: CGFloat = 10
private static let projectRowHeight: CGFloat = 31
private static let projectRowSpacing: CGFloat = 5
private static let maxVisibleProjectRows = 5
private static let projectSourceRowHeight: CGFloat = 29
private static let projectSourceSpacing: CGFloat = 3
private static let projectSourceIndent: CGFloat = 10
private static let projectMoreRowHeight: CGFloat = 16
private static let maxVisibleProjectSourceRows = 2
static let verticalPadding: CGFloat = 10
static func windowLabel(days: Int) -> String {
if days == 1 {
return L("Today")
}
return String(format: L("Last %d days"), days)
}
private static func accentHeight(for row: DetailRow, rowHeight: CGFloat) -> CGFloat {
row.subtitle == nil && row.modeSubtitle == nil ? 14 : rowHeight
}
private static func capHeight(maxValue: Double) -> Double {
maxValue * 0.05
}
/// Y-axis tick values for the cost chart: 0, mid, max when the range is at
/// $1 or more; 0 and max for smaller ranges; empty for flat/no data so the
/// axis renders no labels.
private static func yAxisTickValues(maxCostUSD: Double) -> [Double] {
guard maxCostUSD > 0 else { return [] }
if maxCostUSD < 1.0 {
return [0, maxCostUSD]
}
return [0, maxCostUSD / 2, maxCostUSD]
}
private static func makeModel(provider: UsageProvider, daily: [DailyEntry]) -> Model {
let sorted = daily.sorted { lhs, rhs in lhs.date < rhs.date }
var points: [Point] = []
points.reserveCapacity(sorted.count)
var pointsByKey: [String: Point] = [:]
pointsByKey.reserveCapacity(sorted.count)
var entriesByKey: [String: DailyEntry] = [:]
entriesByKey.reserveCapacity(sorted.count)
var dateKeys: [(key: String, date: Date)] = []
dateKeys.reserveCapacity(sorted.count)
var peak: (key: String, costUSD: Double)?
var maxCostUSD: Double = 0
var maxDetailRows = 0
var hasModeDetails = false
for entry in sorted {
guard let (costUSD, date) = self.chartPointInput(for: entry) else { continue }
let point = Point(
date: date,
costUSD: costUSD,
totalTokens: entry.totalTokens,
requestCount: entry.requestCount)
points.append(point)
pointsByKey[entry.date] = point
entriesByKey[entry.date] = entry
dateKeys.append((entry.date, date))
let modelBreakdowns = entry.modelBreakdowns ?? []
maxDetailRows = max(maxDetailRows, modelBreakdowns.count)
hasModeDetails = hasModeDetails || modelBreakdowns.contains { Self.hasModeSubtitle($0) }
if let cur = peak {
if costUSD > cur.costUSD {
peak = (entry.date, costUSD)
}
} else {
peak = (entry.date, costUSD)
}
maxCostUSD = max(maxCostUSD, costUSD)
}
let axisDates: [Date] = {
guard let first = dateKeys.first?.date, let last = dateKeys.last?.date else { return [] }
if Calendar.current.isDate(first, inSameDayAs: last) {
return [first]
}
return [first, last]
}()
let barColor = Self.barColor(for: provider)
return Model(
points: points,
pointsByDateKey: pointsByKey,
entriesByDateKey: entriesByKey,
dateKeys: dateKeys,
axisDates: axisDates,
barColor: barColor,
peakKey: maxCostUSD > 0 ? peak?.key : nil,
maxCostUSD: maxCostUSD,
detailViewportRowCount: min(maxDetailRows, self.maxVisibleDetailLines),
hasDetailOverflow: maxDetailRows > self.maxVisibleDetailLines,
detailRowHeight: hasModeDetails ? self.expandedDetailRowHeight : self.compactDetailRowHeight)
}
private static func axisLabelPlacement(for dates: [Date]) -> AxisLabelPlacement {
switch dates.count {
case 0: .hidden
case 1: .centered
default: .edges
}
}
private static func xAxisLabelAnchor(for date: Date, axisDates: [Date]) -> UnitPoint {
switch self.axisLabelPlacement(for: axisDates) {
case .hidden, .centered:
.top
case .edges:
if let first = axisDates.first, Calendar.current.isDate(date, inSameDayAs: first) {
.topLeading
} else if let last = axisDates.last, Calendar.current.isDate(date, inSameDayAs: last) {
.topTrailing
} else {
.top
}
}
}
private static func barColor(for provider: UsageProvider) -> Color {
let color = ProviderDescriptorRegistry.descriptor(for: provider).branding.color
return Color(red: color.red, green: color.green, blue: color.blue)
}
private static func dateFromDayKey(_ key: String) -> Date? {
let parts = key.split(separator: "-")
guard parts.count == 3,
let year = Int(parts[0]),
let month = Int(parts[1]),
let day = Int(parts[2]) else { return nil }
var comps = DateComponents()
comps.calendar = Calendar.current
comps.timeZone = TimeZone.current
comps.year = year
comps.month = month
comps.day = day
comps.hour = 12
return comps.date
}
private static func chartPointInput(for entry: DailyEntry) -> (costUSD: Double, date: Date)? {
guard let costUSD = entry.costUSD, costUSD >= 0 else { return nil }
guard let date = self.dateFromDayKey(entry.date) else { return nil }
return (costUSD, date)
}
private static func peakPoint(model: Model) -> Point? {
guard let key = model.peakKey else { return nil }
return model.pointsByDateKey[key]
}
private static func hasModeSubtitle(_ item: CostUsageDailyReport.ModelBreakdown) -> Bool {
item.standardCostUSD != nil || item.priorityCostUSD != nil
}
private static func detailRowsViewportHeight(rowCount: Int, rowHeight: CGFloat) -> CGFloat {
guard rowCount > 0 else { return 0 }
return CGFloat(rowCount) * rowHeight + CGFloat(rowCount - 1) * self.detailSpacing
}
private static func detailBlockHeight(rowCount: Int, hasOverflow: Bool, rowHeight: CGFloat) -> CGFloat {
guard rowCount > 0 else { return self.detailPrimaryLineHeight }
var height = self.detailPrimaryLineHeight + self.detailSpacing
height += self.detailRowsViewportHeight(rowCount: rowCount, rowHeight: rowHeight)
if hasOverflow {
height += self.detailSpacing + self.detailHintHeight
}
return height
}
private static func projectBlockHeight(projects: [CostUsageProjectBreakdown]) -> CGFloat {
let visibleProjects = Array(projects.prefix(self.maxVisibleProjectRows))
guard !visibleProjects.isEmpty else { return 0 }
return self.detailPrimaryLineHeight
+ self.projectRowSpacing
+ visibleProjects.reduce(CGFloat(0)) { $0 + self.projectEntryHeight($1) }
+ CGFloat(max(visibleProjects.count - 1, 0)) * self.projectRowSpacing
}
private static func projectEntryHeight(_ project: CostUsageProjectBreakdown) -> CGFloat {
let sources = self.visibleProjectSources(project)
guard !sources.isEmpty else { return self.projectRowHeight }
let visibleSources = min(sources.count, self.maxVisibleProjectSourceRows)
let moreRows = sources.count > self.maxVisibleProjectSourceRows ? 1 : 0
return self.projectRowHeight
+ CGFloat(visibleSources) * (self.projectSourceRowHeight + self.projectSourceSpacing)
+ CGFloat(moreRows) * (self.projectMoreRowHeight + self.projectSourceSpacing)
}
static func visibleProjectSources(
_ project: CostUsageProjectBreakdown) -> [CostUsageProjectSourceBreakdown]
{
guard project.sources.count == 1 else { return project.sources }
guard let source = project.sources.first, source.path != project.path else { return [] }
return [source]
}
private static func defaultSelectedDateKey(model: Model) -> String? {
model.dateKeys.last?.key
}
private func selectionBandRect(model: Model, proxy: ChartProxy, geo: GeometryProxy) -> CGRect? {
guard let key = self.selectedDateKey else { return nil }
guard let plotAnchor = proxy.plotFrame else { return nil }
let plotFrame = geo[plotAnchor]
guard let index = model.dateKeys.firstIndex(where: { $0.key == key }) else { return nil }
let date = model.dateKeys[index].date
guard let x = proxy.position(forX: date) else { return nil }
// Use the calendar day slot width so the band stays the same size regardless of data gaps.
let nextDayX = proxy.position(forX: ChartBarHoverSelection.nextCalendarDay(after: date)) ?? (x + 20)
let slotWidth = abs(nextDayX - x)
let barHalfWidth = slotWidth * 0.25 + 2
let left = plotFrame.origin.x + x - barHalfWidth
let right = plotFrame.origin.x + x + barHalfWidth
return CGRect(x: left, y: plotFrame.origin.y, width: right - left, height: plotFrame.height)
}
private func updateSelection(
location: CGPoint?,
model: Model,
proxy: ChartProxy,
geo: GeometryProxy)
{
// Keep the last hovered day selected when the pointer leaves the chart so the adjacent
// model-breakdown scroller remains interactive. The selection resets with the menu view.
guard let location else { return }
guard let plotAnchor = proxy.plotFrame else { return }
let plotFrame = geo[plotAnchor]
guard plotFrame.contains(location) else { return }
let xInPlot = location.x - plotFrame.origin.x
guard let date: Date = proxy.value(atX: xInPlot) else { return }
guard let nearest = self.nearestDateKey(to: date, model: model) else { return }
// Stay on the last selected bar when cursor is in the gap between bars.
if let nearestEntry = model.dateKeys.first(where: { $0.key == nearest }),
let barX = proxy.position(forX: nearestEntry.date)
{
let nextDayX = proxy.position(forX: ChartBarHoverSelection.nextCalendarDay(after: nearestEntry.date)) ??
(barX + 20)
let slotWidth = abs(nextDayX - barX)
guard ChartBarHoverSelection.accepts(
distanceFromBarCenter: abs(location.x - (plotFrame.origin.x + barX)),
barHalfWidth: slotWidth * 0.25 + 2,
selectableCount: model.dateKeys.count)
else { return }
}
if self.selectedDateKey != nearest {
self.selectedDateKey = nearest
}
}
private func projectSummary(_ project: CostUsageProjectBreakdown) -> String {
let cost = project.totalCostUSD
.map { self.costString($0) } ?? ""
guard let totalTokens = project.totalTokens else { return cost }
return "\(cost) · \(L("%@ tokens", UsageFormatter.tokenCountString(totalTokens)))"
}
private func projectParentRow(_ project: CostUsageProjectBreakdown) -> some View {
VStack(alignment: .leading, spacing: 1) {
HStack(spacing: 8) {
Text(project.name)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.tail)
Spacer(minLength: 8)
Text(self.projectSummary(project))
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineLimit(1)
.truncationMode(.head)
}
if let path = project.path {
Text(path)
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineLimit(1)
.truncationMode(.middle)
}
}
.frame(height: Self.projectRowHeight, alignment: .leading)
}
private func projectSourceRow(_ source: CostUsageProjectSourceBreakdown) -> some View {
VStack(alignment: .leading, spacing: 1) {
HStack(spacing: 6) {
Text(source.name)
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineLimit(1)
.truncationMode(.tail)
Spacer(minLength: 6)
Text(self.projectSourceSummary(source))
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineLimit(1)
.truncationMode(.head)
}
if let path = source.path {
Text(path)
.font(.caption2)
.foregroundStyle(Color(nsColor: .quaternaryLabelColor))
.lineLimit(1)
.truncationMode(.middle)
}
}
.padding(.leading, Self.projectSourceIndent)
.frame(height: Self.projectSourceRowHeight, alignment: .leading)
}
private func projectSourceSummary(_ source: CostUsageProjectSourceBreakdown) -> String {
let cost = source.totalCostUSD
.map { self.costString($0) } ?? ""
guard let totalTokens = source.totalTokens else { return cost }
return "\(cost) · \(L("%@ tokens", UsageFormatter.tokenCountString(totalTokens)))"
}
private func nearestDateKey(to date: Date, model: Model) -> String? {
guard !model.dateKeys.isEmpty else { return nil }
var best: (key: String, distance: TimeInterval)?
for entry in model.dateKeys {
let dist = abs(entry.date.timeIntervalSince(date))
if let cur = best {
if dist < cur.distance {
best = (entry.key, dist)
}
} else {
best = (entry.key, dist)
}
}
return best?.key
}
private func detailContent(selectedDateKey: String?, model: Model) -> DetailContent {
guard let key = selectedDateKey,
let point = model.pointsByDateKey[key],
let date = Self.dateFromDayKey(key)
else {
return DetailContent(primary: L("Hover a bar for details"), rows: [])
}
let dayLabel = date.formatted(.dateTime.month(.abbreviated).day())
let cost = self.costString(point.costUSD)
var parts = [cost]
if let tokens = point.totalTokens {
parts.append("\(UsageFormatter.tokenCountString(tokens)) tokens")
}
if let requests = point.requestCount {
parts.append("\(UsageFormatter.tokenCountString(requests)) requests")
}
let primary = "\(dayLabel): \(parts.joined(separator: " · "))"
return DetailContent(primary: primary, rows: self.breakdownRows(key: key, model: model))
}
private func breakdownRows(key: String, model: Model) -> [DetailRow] {
guard let entry = model.entriesByDateKey[key] else { return [] }
guard let breakdown = entry.modelBreakdowns, !breakdown.isEmpty else { return [] }
return Self.orderedBreakdownItems(breakdown)
.enumerated()
.map { index, item in
DetailRow(
id: "\(item.modelName)-\(index)",
title: UsageFormatter.modelDisplayName(item.modelName),
subtitle: self.modelBreakdownTotalSubtitle(item),
modeSubtitle: self.modelBreakdownModeSubtitle(item),
accentColor: model.barColor.opacity(Self.breakdownAccentOpacity(for: index)))
}
}
static func orderedBreakdownItems(
_ breakdown: [CostUsageDailyReport.ModelBreakdown]) -> [CostUsageDailyReport.ModelBreakdown]
{
breakdown.sorted { lhs, rhs in
let lCost = lhs.costUSD ?? -1
let rCost = rhs.costUSD ?? -1
if lCost != rCost {
return lCost > rCost
}
let lTokens = lhs.totalTokens ?? -1
let rTokens = rhs.totalTokens ?? -1
if lTokens != rTokens {
return lTokens > rTokens
}
return lhs.modelName > rhs.modelName
}
}
static func detailViewportRowCount(itemCount: Int) -> Int {
min(max(itemCount, 0), self.maxVisibleDetailLines)
}
static func detailRowsNeedScrolling(itemCount: Int) -> Bool {
itemCount > self.maxVisibleDetailLines
}
static func detailOverflowHint(itemCount: Int) -> String? {
self.detailRowsNeedScrolling(itemCount: itemCount) ? L("Scroll to see more models") : nil
}
private func modelBreakdownTotalSubtitle(_ item: CostUsageDailyReport.ModelBreakdown) -> String? {
UsageFormatter.modelCostDetail(
item.modelName,
costUSD: item.costUSD,
totalTokens: item.totalTokens,
currencyCode: self.currencyCode)
}
private func modelBreakdownModeSubtitle(_ item: CostUsageDailyReport.ModelBreakdown) -> String? {
var parts: [String] = []
if let standardCost = item.standardCostUSD {
var standardPart = "Std \(self.costString(standardCost))"
if let standardTokens = item.standardTokens {
standardPart += " · \(UsageFormatter.tokenCountString(standardTokens))"
}
parts.append(standardPart)
}
if let priorityCost = item.priorityCostUSD {
var priorityPart = "Fast \(self.costString(priorityCost))"
if let priorityTokens = item.priorityTokens {
priorityPart += " · \(UsageFormatter.tokenCountString(priorityTokens))"
}
parts.append(priorityPart)
}
guard !parts.isEmpty else { return nil }
return parts.joined(separator: " / ")
}
private func costString(_ value: Double) -> String {
Self.costString(value, currencyCode: self.currencyCode)
}
private static func costString(_ value: Double, currencyCode: String) -> String {
UsageFormatter.currencyString(value, currencyCode: currencyCode)
}
private static func yAxisCostString(_ value: Double, currencyCode: String) -> String {
UsageFormatter.compactCurrencyString(value, currencyCode: currencyCode)
}
private static func breakdownAccentOpacity(for index: Int) -> Double {
let opacity = 0.75 - (Double(index) * 0.12)
return max(0.3, opacity)
}
}
extension CostHistoryChartMenuView {
struct RenderFingerprint: Equatable {
let currencyCode: String
let historyDays: Int
let windowLabel: String?
let totalCostBitPattern: UInt64?
let hasDailyEntries: Bool
let daily: [VisibleDailyFingerprint]
let projects: [VisibleProjectFingerprint]
}
struct VisibleDailyFingerprint: Equatable {
let date: String
let totalTokens: Int?
let requestCount: Int?
let costBitPattern: UInt64?
let modelBreakdowns: [VisibleModelBreakdownFingerprint]
}
struct VisibleModelBreakdownFingerprint: Equatable {
let modelName: String
let costBitPattern: UInt64?
let totalTokens: Int?
let standardCostBitPattern: UInt64?
let priorityCostBitPattern: UInt64?
let standardTokens: Int?
let priorityTokens: Int?
}
struct VisibleProjectFingerprint: Equatable {
let name: String
let path: String?
let totalTokens: Int?
let totalCostBitPattern: UInt64?
let visibleSourceCount: Int
let sources: [VisibleSourceFingerprint]
}
struct VisibleSourceFingerprint: Equatable {
let name: String
let path: String?
let totalTokens: Int?
let totalCostBitPattern: UInt64?
}
static func renderFingerprint(
from snapshot: CostUsageTokenSnapshot,
provider: UsageProvider) -> RenderFingerprint
{
let projects = provider == .codex ? snapshot.projects : []
return RenderFingerprint(
currencyCode: snapshot.currencyCode,
historyDays: snapshot.historyDays,
windowLabel: snapshot.historyLabel,
totalCostBitPattern: snapshot.last30DaysCostUSD.map(\.bitPattern),
hasDailyEntries: !snapshot.daily.isEmpty,
daily: snapshot.daily
.filter { self.chartPointInput(for: $0) != nil }
.sorted { $0.date < $1.date }
.map(self.visibleDailyFingerprint),
projects: Array(projects.prefix(self.maxVisibleProjectRows)).map { project in
let visibleSources = self.visibleProjectSources(project)
return VisibleProjectFingerprint(
name: project.name,
path: project.path,
totalTokens: project.totalTokens,
totalCostBitPattern: project.totalCostUSD.map(\.bitPattern),
visibleSourceCount: visibleSources.count,
sources: Array(visibleSources.prefix(self.maxVisibleProjectSourceRows)).map { source in
VisibleSourceFingerprint(
name: source.name,
path: source.path,
totalTokens: source.totalTokens,
totalCostBitPattern: source.totalCostUSD.map(\.bitPattern))
})
})
}
private static func visibleDailyFingerprint(_ entry: DailyEntry) -> VisibleDailyFingerprint {
VisibleDailyFingerprint(
date: entry.date,
totalTokens: entry.totalTokens,
requestCount: entry.requestCount,
costBitPattern: entry.costUSD.map(\.bitPattern),
modelBreakdowns: self.orderedBreakdownItems(entry.modelBreakdowns ?? []).map { item in
VisibleModelBreakdownFingerprint(
modelName: item.modelName,
costBitPattern: item.costUSD.map(\.bitPattern),
totalTokens: item.totalTokens,
standardCostBitPattern: item.standardCostUSD.map(\.bitPattern),
priorityCostBitPattern: item.priorityCostUSD.map(\.bitPattern),
standardTokens: item.standardCostUSD == nil ? nil : item.standardTokens,
priorityTokens: item.priorityCostUSD == nil ? nil : item.priorityTokens)
})
}
static func _defaultSelectedDateKeyForTesting(provider: UsageProvider, daily: [DailyEntry]) -> String? {
self.defaultSelectedDateKey(model: self.makeModel(provider: provider, daily: daily))
}
static func _axisDatesForTesting(provider: UsageProvider, daily: [DailyEntry]) -> [Date] {
self.makeModel(provider: provider, daily: daily).axisDates
}
static func _axisLabelPlacementForTesting(
provider: UsageProvider,
daily: [DailyEntry]) -> AxisLabelPlacement
{
self.axisLabelPlacement(for: self.makeModel(provider: provider, daily: daily).axisDates)
}
static func _yAxisTickValuesForTesting(maxCostUSD: Double) -> [Double] {
self.yAxisTickValues(maxCostUSD: maxCostUSD)
}
static func _yAxisCostStringForTesting(_ value: Double, currencyCode: String = "USD") -> String {
self.yAxisCostString(value, currencyCode: currencyCode)
}
static func _detailViewportConfigurationForTesting(
provider: UsageProvider,
daily: [DailyEntry]) -> (rowCount: Int, hasOverflow: Bool, rowHeight: CGFloat)
{
let model = self.makeModel(provider: provider, daily: daily)
return (model.detailViewportRowCount, model.hasDetailOverflow, model.detailRowHeight)
}
}
extension CostUsageProjectBreakdown {
fileprivate var projectRowID: String {
self.path ?? "unknown:\(self.name)"
}
}
extension CostUsageProjectSourceBreakdown {
fileprivate var sourceRowID: String {
self.path ?? "unknown:\(self.name)"
}
}
@@ -0,0 +1,334 @@
import Charts
import CodexBarCore
import SwiftUI
@MainActor
struct CreditsHistoryChartMenuView: View {
private struct Point: Identifiable {
let id: String
let date: Date
let creditsUsed: Double
init(date: Date, creditsUsed: Double) {
self.date = date
self.creditsUsed = creditsUsed
self.id = "\(Int(date.timeIntervalSince1970))-\(creditsUsed)"
}
}
private let breakdown: [OpenAIDashboardDailyBreakdown]
private let width: CGFloat
@State private var selectedDayKey: String?
init(breakdown: [OpenAIDashboardDailyBreakdown], width: CGFloat) {
self.breakdown = breakdown
self.width = width
}
var body: some View {
let model = Self.makeModel(from: self.breakdown)
VStack(alignment: .leading, spacing: 10) {
if model.points.isEmpty {
Text(L("No credits history data."))
.font(.footnote)
.foregroundStyle(.secondary)
.accessibilityLabel(L("No credits history data available."))
} else {
Chart {
ForEach(model.points) { point in
BarMark(
x: .value(L("Day"), point.date, unit: .day),
y: .value(L("Credits used"), point.creditsUsed))
.foregroundStyle(Self.barColor)
}
if let peak = Self.peakPoint(model: model) {
let capStart = max(peak.creditsUsed - Self.capHeight(maxValue: model.maxCreditsUsed), 0)
BarMark(
x: .value(L("Day"), peak.date, unit: .day),
yStart: .value(L("Cap start"), capStart),
yEnd: .value(L("Cap end"), peak.creditsUsed))
.foregroundStyle(Color(nsColor: .systemYellow))
}
}
.chartYAxis(.hidden)
.chartXAxis {
AxisMarks(values: model.axisDates) { _ in
AxisGridLine().foregroundStyle(Color.clear)
AxisTick().foregroundStyle(Color.clear)
AxisValueLabel(format: .dateTime.month(.abbreviated).day())
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
}
}
.chartLegend(.hidden)
.frame(height: 130)
.accessibilityLabel(L("Credits history chart"))
.accessibilityValue(
model.points.isEmpty
? L("No data")
: String(format: L("%d days of credits data"), model.points.count))
.chartOverlay { proxy in
GeometryReader { geo in
ZStack(alignment: .topLeading) {
if let rect = self.selectionBandRect(model: model, proxy: proxy, geo: geo) {
Rectangle()
.fill(Self.selectionBandColor)
.frame(width: rect.width, height: rect.height)
.position(x: rect.midX, y: rect.midY)
.allowsHitTesting(false)
}
MouseLocationReader { location in
self.updateSelection(location: location, model: model, proxy: proxy, geo: geo)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
}
}
}
let detail = self.detailLines(model: model)
VStack(alignment: .leading, spacing: 0) {
Text(detail.primary)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.tail)
.frame(height: 16, alignment: .leading)
Text(detail.secondary ?? " ")
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.tail)
.frame(height: 16, alignment: .leading)
.opacity(detail.secondary == nil ? 0 : 1)
}
if let total = model.totalCreditsUsed {
Text(String(
format: L("Total (30d): %@ credits"),
total.formatted(.number.precision(.fractionLength(0...2)))))
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
.padding(.horizontal, 16)
.padding(.vertical, 10)
.frame(minWidth: self.width, maxWidth: .infinity, alignment: .leading)
}
private struct Model {
let points: [Point]
let breakdownByDayKey: [String: OpenAIDashboardDailyBreakdown]
let pointsByDayKey: [String: Point]
let dayDates: [(dayKey: String, date: Date)]
let selectableDayDates: [(dayKey: String, date: Date)]
let axisDates: [Date]
let peakKey: String?
let totalCreditsUsed: Double?
let maxCreditsUsed: Double
}
private static let barColor = Color(red: 73 / 255, green: 163 / 255, blue: 176 / 255)
private static let selectionBandColor = Color(nsColor: .labelColor).opacity(0.1)
private static func capHeight(maxValue: Double) -> Double {
maxValue * 0.05
}
private static func makeModel(from breakdown: [OpenAIDashboardDailyBreakdown]) -> Model {
let sorted = breakdown.sorted { lhs, rhs in lhs.day < rhs.day }
var points: [Point] = []
points.reserveCapacity(sorted.count)
var breakdownByDayKey: [String: OpenAIDashboardDailyBreakdown] = [:]
breakdownByDayKey.reserveCapacity(sorted.count)
var pointsByDayKey: [String: Point] = [:]
pointsByDayKey.reserveCapacity(sorted.count)
var dayDates: [(dayKey: String, date: Date)] = []
dayDates.reserveCapacity(sorted.count)
var selectableDayDates: [(dayKey: String, date: Date)] = []
selectableDayDates.reserveCapacity(sorted.count)
var totalCreditsUsed: Double = 0
var peak: (key: String, creditsUsed: Double)?
var maxCreditsUsed: Double = 0
for day in sorted {
guard let date = self.dateFromDayKey(day.day) else { continue }
breakdownByDayKey[day.day] = day
dayDates.append((dayKey: day.day, date: date))
totalCreditsUsed += day.totalCreditsUsed
if day.totalCreditsUsed > 0 {
let point = Point(date: date, creditsUsed: day.totalCreditsUsed)
points.append(point)
pointsByDayKey[day.day] = point
selectableDayDates.append((dayKey: day.day, date: date))
if let cur = peak {
if day.totalCreditsUsed > cur.creditsUsed { peak = (day.day, day.totalCreditsUsed) }
} else {
peak = (day.day, day.totalCreditsUsed)
}
maxCreditsUsed = max(maxCreditsUsed, day.totalCreditsUsed)
}
}
let axisDates: [Date] = {
guard let first = dayDates.first?.date, let last = dayDates.last?.date else { return [] }
if Calendar.current.isDate(first, inSameDayAs: last) { return [first] }
return [first, last]
}()
return Model(
points: points,
breakdownByDayKey: breakdownByDayKey,
pointsByDayKey: pointsByDayKey,
dayDates: dayDates,
selectableDayDates: selectableDayDates,
axisDates: axisDates,
peakKey: peak?.key,
totalCreditsUsed: totalCreditsUsed > 0 ? totalCreditsUsed : nil,
maxCreditsUsed: maxCreditsUsed)
}
private static func dateFromDayKey(_ key: String) -> Date? {
let parts = key.split(separator: "-")
guard parts.count == 3,
let year = Int(parts[0]),
let month = Int(parts[1]),
let day = Int(parts[2])
else {
return nil
}
var comps = DateComponents()
comps.calendar = Calendar.current
comps.timeZone = TimeZone.current
comps.year = year
comps.month = month
comps.day = day
comps.hour = 12
return comps.date
}
private static func peakPoint(model: Model) -> Point? {
guard let key = model.peakKey else { return nil }
return model.pointsByDayKey[key]
}
private func selectionBandRect(model: Model, proxy: ChartProxy, geo: GeometryProxy) -> CGRect? {
guard let key = self.selectedDayKey else { return nil }
guard let plotAnchor = proxy.plotFrame else { return nil }
let plotFrame = geo[plotAnchor]
guard let index = model.dayDates.firstIndex(where: { $0.dayKey == key }) else { return nil }
let date = model.dayDates[index].date
guard let x = proxy.position(forX: date) else { return nil }
if model.dayDates.count <= 1 {
return CGRect(
x: plotFrame.origin.x,
y: plotFrame.origin.y,
width: plotFrame.width,
height: plotFrame.height)
}
// Use the calendar day slot width (always 1 day on the time axis) so the band is the
// same size for every bar regardless of gaps in the data.
let nextDayX = proxy.position(forX: ChartBarHoverSelection.nextCalendarDay(after: date)) ?? (x + 20)
let slotWidth = abs(nextDayX - x)
let barHalfWidth = slotWidth * 0.25 + 2
let left = plotFrame.origin.x + x - barHalfWidth
let right = plotFrame.origin.x + x + barHalfWidth
return CGRect(x: left, y: plotFrame.origin.y, width: right - left, height: plotFrame.height)
}
private func updateSelection(
location: CGPoint?,
model: Model,
proxy: ChartProxy,
geo: GeometryProxy)
{
guard let location else {
if self.selectedDayKey != nil { self.selectedDayKey = nil }
return
}
guard let plotAnchor = proxy.plotFrame else { return }
let plotFrame = geo[plotAnchor]
guard plotFrame.contains(location) else { return }
let xInPlot = location.x - plotFrame.origin.x
guard let date: Date = proxy.value(atX: xInPlot) else { return }
guard let nearest = self.nearestDayKey(to: date, model: model) else { return }
// Stay on the last selected bar when cursor is in the gap between bars; only switch
// selection when the cursor is over the bar's own visual body.
// Skip this gate for single-day charts: no gap exists, and selectionBandRect
// already covers the full plot width in that case.
if model.selectableDayDates.count > 1,
let nearestEntry = model.selectableDayDates.first(where: { $0.dayKey == nearest }),
let barX = proxy.position(forX: nearestEntry.date)
{
let nextDayX = proxy.position(forX: ChartBarHoverSelection.nextCalendarDay(after: nearestEntry.date)) ??
(barX + 20)
let slotWidth = abs(nextDayX - barX)
guard ChartBarHoverSelection.accepts(
distanceFromBarCenter: abs(location.x - (plotFrame.origin.x + barX)),
barHalfWidth: slotWidth * 0.25 + 2,
selectableCount: model.selectableDayDates.count)
else { return }
}
if self.selectedDayKey != nearest {
self.selectedDayKey = nearest
}
}
private func nearestDayKey(to date: Date, model: Model) -> String? {
guard !model.selectableDayDates.isEmpty else { return nil }
var best: (key: String, distance: TimeInterval)?
for entry in model.selectableDayDates {
let dist = abs(entry.date.timeIntervalSince(date))
if let cur = best {
if dist < cur.distance { best = (entry.dayKey, dist) }
} else {
best = (entry.dayKey, dist)
}
}
return best?.key
}
private func detailLines(model: Model) -> (primary: String, secondary: String?) {
guard let key = self.selectedDayKey,
let day = model.breakdownByDayKey[key],
let date = Self.dateFromDayKey(key)
else {
return (L("Hover a bar for details"), nil)
}
let dayLabel = date.formatted(.dateTime.month(.abbreviated).day())
let total = day.totalCreditsUsed.formatted(.number.precision(.fractionLength(0...2)))
if day.services.isEmpty {
return (String(format: L("%@: %@ credits"), dayLabel, total), nil)
}
if day.services.count <= 1, let first = day.services.first {
let used = first.creditsUsed.formatted(.number.precision(.fractionLength(0...2)))
return (String(format: L("%@: %@ credits"), dayLabel, used), first.service)
}
let services = day.services
.sorted { lhs, rhs in
if lhs.creditsUsed == rhs.creditsUsed { return lhs.service < rhs.service }
return lhs.creditsUsed > rhs.creditsUsed
}
.prefix(3)
.map { "\($0.service) \($0.creditsUsed.formatted(.number.precision(.fractionLength(0...2))))" }
.joined(separator: " · ")
return (String(format: L("%@: %@ credits"), dayLabel, total), services)
}
}
+117
View File
@@ -0,0 +1,117 @@
import AppKit
import CodexBarCore
import Foundation
/// Opens Cursor in the user's browser and waits until the normal browser-cookie importer can read a session.
@MainActor
final class CursorLoginRunner {
enum Phase {
case loading
case waitingLogin
case success
case failed(String)
}
struct Result {
enum Outcome {
case success
case cancelled
case failed(String)
}
let outcome: Outcome
let email: String?
}
typealias SnapshotLoader = @Sendable () async throws -> CursorStatusSnapshot
typealias Sleeper = @Sendable (UInt64) async throws -> Void
typealias SessionCacheResetter = @Sendable () async -> Void
private let loadSnapshot: SnapshotLoader
private let openURL: @MainActor (URL) -> Bool
private let sleeper: Sleeper
private let resetSessionCache: SessionCacheResetter
private let timeout: TimeInterval
private let pollInterval: TimeInterval
private let logger = CodexBarLog.logger(LogCategories.cursorLogin)
static let authURL = URL(string: "https://authenticator.cursor.sh/")!
init(
browserDetection: BrowserDetection,
timeout: TimeInterval = 120,
pollInterval: TimeInterval = 2,
openURL: @escaping @MainActor (URL) -> Bool = { NSWorkspace.shared.open($0) },
loadSnapshot: SnapshotLoader? = nil,
sleeper: @escaping Sleeper = { try await Task.sleep(nanoseconds: $0) },
resetSessionCache: @escaping SessionCacheResetter = {
CookieHeaderCache.clear(provider: .cursor)
CursorSessionStore.shared.clearCookies()
})
{
self.timeout = timeout
self.pollInterval = pollInterval
self.openURL = openURL
self.sleeper = sleeper
self.resetSessionCache = resetSessionCache
self.loadSnapshot = loadSnapshot ?? {
let probe = CursorStatusProbe(browserDetection: browserDetection)
return try await probe.fetch(
allowCachedSessions: false,
allowAppAuthFallback: false)
}
}
func run(onPhaseChange: @escaping @MainActor (Phase) -> Void) async -> Result {
onPhaseChange(.loading)
self.logger.info("Cursor login started")
await self.resetSessionCache()
guard self.openURL(Self.authURL) else {
let message = L("Could not open Cursor login in your browser.")
onPhaseChange(.failed(message))
self.logger.error("Cursor login browser launch failed")
return Result(outcome: .failed(message), email: nil)
}
onPhaseChange(.waitingLogin)
let deadline = Date().addingTimeInterval(self.timeout)
var lastError: Error?
repeat {
if Task.isCancelled {
self.logger.info("Cursor login cancelled")
return Result(outcome: .cancelled, email: nil)
}
do {
let snapshot = try await self.loadSnapshot()
onPhaseChange(.success)
self.logger.info("Cursor login completed", metadata: ["outcome": "success"])
return Result(outcome: .success, email: snapshot.accountEmail)
} catch {
lastError = error
}
guard Date() < deadline else { break }
let delay = UInt64(max(0.1, self.pollInterval) * 1_000_000_000)
try? await self.sleeper(delay)
} while true
let message = Self.timeoutMessage(lastError: lastError)
onPhaseChange(.failed(message))
self.logger.warning("Cursor login timed out", metadata: ["error": message])
return Result(outcome: .failed(message), email: nil)
}
private static func timeoutMessage(lastError: Error?) -> String {
let hint = L("Sign in to cursor.com in your browser, then refresh Cursor in CodexBar.")
guard let lastError else {
return String(format: L("Timed out waiting for Cursor login. %@"), hint)
}
return String(
format: L("Timed out waiting for Cursor login. %@ Last error: %@"),
hint,
lastError.localizedDescription)
}
}
@@ -0,0 +1,23 @@
import Foundation
enum RelativeTimeFormatters {
@MainActor
static func full(locale: Locale) -> RelativeDateTimeFormatter {
let formatter = RelativeDateTimeFormatter()
formatter.locale = locale
formatter.unitsStyle = .full
return formatter
}
}
extension Date {
@MainActor
func relativeDescription(now: Date = .now) -> String {
let seconds = abs(now.timeIntervalSince(self))
if seconds < 15 {
return L("just now")
}
let locale = codexBarLocalizedLocale()
return RelativeTimeFormatters.full(locale: locale).localizedString(for: self, relativeTo: now)
}
}
+96
View File
@@ -0,0 +1,96 @@
import AppKit
import CoreVideo
import Observation
import QuartzCore
/// Minimal display link driver using NSScreen.displayLink on macOS 15+,
/// and CVDisplayLink on macOS 14.
/// Publishes ticks on the main thread at the requested frame rate.
@MainActor
@Observable
final class DisplayLinkDriver {
// Published counter used to drive SwiftUI updates.
var tick: Int = 0
private var displayLink: CADisplayLink?
private var cvDisplayLink: CVDisplayLink?
private var targetInterval: CFTimeInterval = 1.0 / 60.0
private var lastTickTimestamp: CFTimeInterval = 0
private let onTick: (() -> Void)?
init(onTick: (() -> Void)? = nil) {
self.onTick = onTick
}
func start(fps: Double = 12) {
guard self.displayLink == nil, self.cvDisplayLink == nil else { return }
let clampedFps = max(fps, 1)
self.targetInterval = 1.0 / clampedFps
self.lastTickTimestamp = 0
if #available(macOS 15, *), let screen = NSScreen.main {
// NSScreen.displayLink is macOS 15+ only.
let displayLink = screen.displayLink(target: self, selector: #selector(self.step))
let rate = Float(clampedFps)
displayLink.preferredFrameRateRange = CAFrameRateRange(
minimum: rate,
maximum: rate,
preferred: rate)
displayLink.add(to: .main, forMode: .common)
self.displayLink = displayLink
} else {
self.startCVDisplayLink()
}
}
func stop() {
self.displayLink?.invalidate()
self.displayLink = nil
if let cvDisplayLink = self.cvDisplayLink {
CVDisplayLinkStop(cvDisplayLink)
}
self.cvDisplayLink = nil
}
@objc private func step(_: AnyObject) {
self.handleTick()
}
private func handleTick() {
let now = CACurrentMediaTime()
if self.lastTickTimestamp > 0, now - self.lastTickTimestamp < self.targetInterval {
return
}
self.lastTickTimestamp = now
// Safe on main runloop; drives SwiftUI updates.
self.tick &+= 1
self.onTick?()
}
private func startCVDisplayLink() {
var link: CVDisplayLink?
if CVDisplayLinkCreateWithActiveCGDisplays(&link) != kCVReturnSuccess {
return
}
guard let link else { return }
let callback: CVDisplayLinkOutputCallback = { _, _, _, _, _, userInfo in
guard let userInfo else { return kCVReturnSuccess }
let driver = Unmanaged<DisplayLinkDriver>.fromOpaque(userInfo).takeUnretainedValue()
driver.scheduleTick()
return kCVReturnSuccess
}
CVDisplayLinkSetOutputCallback(link, callback, Unmanaged.passUnretained(self).toOpaque())
CVDisplayLinkStart(link)
self.cvDisplayLink = link
}
private nonisolated func scheduleTick() {
Task { @MainActor [weak self] in
self?.handleTick()
}
}
deinit {
Task { @MainActor [weak self] in
self?.stop()
}
}
}
+95
View File
@@ -0,0 +1,95 @@
import AppKit
import CodexBarCore
import Foundation
enum GeminiLoginRunner {
private static let geminiConfigDir = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".gemini")
private static let credentialsFile = "oauth_creds.json"
private static func clearCredentials() {
let fm = FileManager.default
let filesToDelete = [credentialsFile, "google_accounts.json"]
for file in filesToDelete {
let path = self.geminiConfigDir.appendingPathComponent(file)
try? fm.removeItem(at: path)
}
}
struct Result {
enum Outcome {
case success
case missingBinary
case launchFailed(String)
}
let outcome: Outcome
}
static func run(onCredentialsCreated: (@Sendable () -> Void)? = nil) async -> Result {
await Task(priority: .userInitiated) {
let env = ProcessInfo.processInfo.environment
guard let binary = BinaryLocator.resolveGeminiBinary(
env: env,
loginPATH: LoginShellPathCache.shared.current)
else {
return Result(outcome: .missingBinary)
}
// Clear existing credentials before auth (enables clean account switch)
Self.clearCredentials()
// Start watching for credentials file to be created
if let callback = onCredentialsCreated {
Self.watchForCredentials(callback: callback)
}
// Create a temporary shell script that runs gemini (auto-prompts for auth when no creds)
let scriptContent = """
#!/bin/bash
cd ~
"\(binary)"
"""
let tempDir = FileManager.default.temporaryDirectory
let scriptURL = tempDir.appendingPathComponent("gemini_login_\(UUID().uuidString).command")
do {
try scriptContent.write(to: scriptURL, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: scriptURL.path)
let config = NSWorkspace.OpenConfiguration()
config.activates = true
try await NSWorkspace.shared.open(scriptURL, configuration: config)
// Clean up script after Terminal has time to read it
let scriptPath = scriptURL.path
DispatchQueue.global().asyncAfter(deadline: .now() + 10) {
try? FileManager.default.removeItem(atPath: scriptPath)
}
return Result(outcome: .success)
} catch {
return Result(outcome: .launchFailed(error.localizedDescription))
}
}.value
}
/// Watch for credentials file to be created, then call callback once
private static func watchForCredentials(callback: @escaping @Sendable () -> Void, timeout: TimeInterval = 300) {
let credsPath = self.geminiConfigDir.appendingPathComponent(self.credentialsFile).path
DispatchQueue.global(qos: .utility).async {
let startTime = Date()
while Date().timeIntervalSince(startTime) < timeout {
if FileManager.default.fileExists(atPath: credsPath) {
// Small delay to ensure file is fully written
Thread.sleep(forTimeInterval: 0.5)
callback()
return
}
Thread.sleep(forTimeInterval: 1.0)
}
}
}
}
+38
View File
@@ -0,0 +1,38 @@
import SwiftUI
struct HiddenWindowView: View {
@Environment(\.openSettings) private var openSettings
var body: some View {
Color.clear
.frame(width: 20, height: 20)
.onReceive(NotificationCenter.default.publisher(for: .codexbarOpenSettings)) { _ in
Task { @MainActor in
self.openSettings()
}
}
.task {
// Migrate keychain items to reduce permission prompts during development (runs off main thread)
await Task.detached(priority: .userInitiated) {
KeychainMigration.migrateIfNeeded()
}.value
}
.onAppear {
if let window = NSApp.windows.first(where: { $0.title == "CodexBarLifecycleKeepalive" }) {
// Make the keepalive window truly invisible and non-interactive.
window.styleMask = [.borderless]
window.collectionBehavior = [.auxiliary, .ignoresCycle, .transient, .canJoinAllSpaces]
window.isExcludedFromWindowsMenu = true
window.level = .floating
window.isOpaque = false
window.alphaValue = 0
window.backgroundColor = .clear
window.hasShadow = false
window.ignoresMouseEvents = true
window.canHide = false
window.setContentSize(NSSize(width: 1, height: 1))
window.setFrameOrigin(NSPoint(x: -5000, y: -5000))
}
}
}
}
+975
View File
@@ -0,0 +1,975 @@
import CodexBarCore
import Foundation
enum HistoricalUsageWindowKind: String, Codable {
case secondary
}
enum HistoricalUsageRecordSource: String, Codable {
case live
case backfill
}
struct HistoricalUsageRecord: Codable {
let v: Int
let provider: UsageProvider
let windowKind: HistoricalUsageWindowKind
let source: HistoricalUsageRecordSource
let accountKey: String?
let sampledAt: Date
let usedPercent: Double
let resetsAt: Date
let windowMinutes: Int
init(
v: Int,
provider: UsageProvider,
windowKind: HistoricalUsageWindowKind,
source: HistoricalUsageRecordSource,
accountKey: String?,
sampledAt: Date,
usedPercent: Double,
resetsAt: Date,
windowMinutes: Int)
{
self.v = v
self.provider = provider
self.windowKind = windowKind
self.source = source
self.accountKey = accountKey
self.sampledAt = sampledAt
self.usedPercent = usedPercent
self.resetsAt = resetsAt
self.windowMinutes = windowMinutes
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.v = try container.decodeIfPresent(Int.self, forKey: .v) ?? 1
self.provider = try container.decode(UsageProvider.self, forKey: .provider)
self.windowKind = try container.decode(HistoricalUsageWindowKind.self, forKey: .windowKind)
self.source = try container.decodeIfPresent(HistoricalUsageRecordSource.self, forKey: .source) ?? .live
self.accountKey = try container.decodeIfPresent(String.self, forKey: .accountKey)
self.sampledAt = try container.decode(Date.self, forKey: .sampledAt)
self.usedPercent = try container.decode(Double.self, forKey: .usedPercent)
self.resetsAt = try container.decode(Date.self, forKey: .resetsAt)
self.windowMinutes = try container.decode(Int.self, forKey: .windowMinutes)
}
}
struct HistoricalWeekProfile {
let resetsAt: Date
let windowMinutes: Int
let curve: [Double]
}
struct CodexHistoricalDataset {
static let gridPointCount = 169
let weeks: [HistoricalWeekProfile]
}
actor HistoricalUsageHistoryStore {
private static let schemaVersion = 1
private static let writeInterval: TimeInterval = 30 * 60
private static let writeDeltaThreshold: Double = 1
private static let retentionDays: TimeInterval = 56 * 24 * 60 * 60
private static let minimumWeekSamples = 6
private static let boundaryCoverageWindow: TimeInterval = 24 * 60 * 60
private static let backfillWindowCapWeeks = 8
private static let backfillCalibrationMinimumUsedPercent = 1.0
private static let backfillCalibrationMinimumCredits = 0.001
private static let backfillSampleFractions: [Double] = (0...14).map { Double($0) / 14.0 }
private static let coverageTolerance: TimeInterval = 16 * 60 * 60
private static let resetBucketSeconds: TimeInterval = 5 * 60
private let fileURL: URL
private var records: [HistoricalUsageRecord] = []
private var loaded = false
init(fileURL: URL? = nil) {
self.fileURL = fileURL ?? HistoricalUsageHistoryStore.defaultFileURL()
}
func loadCodexDataset(accountKey: String?) -> CodexHistoricalDataset? {
self.ensureLoaded()
return self.buildDataset(accountKey: accountKey)
}
func loadCodexDataset(
canonicalAccountKey: String?,
canonicalEmailHashKey: String?,
legacyEmailHash: String?,
hasAdjacentMultiAccountVeto: Bool) -> CodexHistoricalDataset?
{
self.ensureLoaded()
return self.buildDataset(
canonicalAccountKey: canonicalAccountKey,
canonicalEmailHashKey: canonicalEmailHashKey,
legacyEmailHash: legacyEmailHash,
hasAdjacentMultiAccountVeto: hasAdjacentMultiAccountVeto)
}
func recordCodexWeekly(
window: RateWindow,
sampledAt: Date = .init(),
accountKey: String?) -> CodexHistoricalDataset?
{
guard let rawResetsAt = window.resetsAt else { return self.loadCodexDataset(accountKey: accountKey) }
guard let windowMinutes = window.windowMinutes, windowMinutes > 0 else {
return self.loadCodexDataset(accountKey: accountKey)
}
self.ensureLoaded()
let resetsAt = Self.normalizeReset(rawResetsAt)
let sample = HistoricalUsageRecord(
v: Self.schemaVersion,
provider: .codex,
windowKind: .secondary,
source: .live,
accountKey: accountKey,
sampledAt: sampledAt,
usedPercent: Self.clamp(window.usedPercent, lower: 0, upper: 100),
resetsAt: resetsAt,
windowMinutes: windowMinutes)
if !self.shouldAccept(sample) {
return self.buildDataset(accountKey: accountKey)
}
self.records.append(sample)
self.pruneOldRecords(now: sampledAt)
self.records.sort { lhs, rhs in
if lhs.sampledAt == rhs.sampledAt {
if lhs.resetsAt == rhs.resetsAt {
return lhs.usedPercent < rhs.usedPercent
}
return lhs.resetsAt < rhs.resetsAt
}
return lhs.sampledAt < rhs.sampledAt
}
self.persist()
return self.buildDataset(accountKey: accountKey)
}
func backfillCodexWeeklyFromUsageBreakdown(
_ breakdown: [OpenAIDashboardDailyBreakdown],
referenceWindow: RateWindow,
now: Date = .init(),
accountKey: String?) -> CodexHistoricalDataset?
{
self.ensureLoaded()
let existingDataset = self.buildDataset(accountKey: accountKey)
guard let rawResetsAt = referenceWindow.resetsAt else { return existingDataset }
guard let windowMinutes = referenceWindow.windowMinutes, windowMinutes > 0 else { return existingDataset }
let resetsAt = Self.normalizeReset(rawResetsAt)
let duration = TimeInterval(windowMinutes) * 60
guard duration > 0 else { return existingDataset }
let windowStart = resetsAt.addingTimeInterval(-duration)
let calibrationEnd = Self.clampDate(now, lower: windowStart, upper: resetsAt)
let dayUsages = Self.parseDayUsages(
from: breakdown,
asOf: calibrationEnd,
fillingFrom: windowStart)
guard !dayUsages.isEmpty else { return existingDataset }
guard let coverageStart = dayUsages.first?.start, let coverageEnd = dayUsages.last?.end else {
return existingDataset
}
guard coverageStart <= windowStart.addingTimeInterval(Self.coverageTolerance) else {
return existingDataset
}
guard coverageEnd >= calibrationEnd.addingTimeInterval(-Self.coverageTolerance) else {
return existingDataset
}
let currentUsedPercent = Self.clamp(referenceWindow.usedPercent, lower: 0, upper: 100)
guard currentUsedPercent >= Self.backfillCalibrationMinimumUsedPercent else { return existingDataset }
let currentCredits = Self.creditsUsed(
from: dayUsages,
between: windowStart,
and: calibrationEnd)
guard currentCredits > Self.backfillCalibrationMinimumCredits else { return existingDataset }
let estimatedCreditsAtLimit = currentCredits / (currentUsedPercent / 100)
guard estimatedCreditsAtLimit.isFinite, estimatedCreditsAtLimit > Self.backfillCalibrationMinimumCredits else {
return existingDataset
}
struct RecordKey: Hashable {
let resetsAt: Date
let sampledAt: Date
let windowMinutes: Int
let accountKey: String?
}
var synthesized: [HistoricalUsageRecord] = []
synthesized.reserveCapacity(Self.backfillWindowCapWeeks * Self.backfillSampleFractions.count)
for weeksBack in 1...Self.backfillWindowCapWeeks {
let reset = Self.normalizeReset(resetsAt.addingTimeInterval(-duration * Double(weeksBack)))
let start = reset.addingTimeInterval(-duration)
guard start >= coverageStart.addingTimeInterval(-Self.coverageTolerance),
reset <= coverageEnd.addingTimeInterval(Self.coverageTolerance)
else {
continue
}
let existingForWeek = self.records.filter {
$0.provider == .codex &&
$0.windowKind == .secondary &&
$0.windowMinutes == windowMinutes &&
$0.accountKey == accountKey &&
$0.resetsAt == reset
}
if Self.isCompleteWeek(samples: existingForWeek, windowStart: start, resetsAt: reset) {
continue
}
var existingRecordKeys = Set(existingForWeek.map {
RecordKey(
resetsAt: $0.resetsAt,
sampledAt: $0.sampledAt,
windowMinutes: $0.windowMinutes,
accountKey: $0.accountKey)
})
let weekCredits = Self.creditsUsed(from: dayUsages, between: start, and: reset)
guard weekCredits > Self.backfillCalibrationMinimumCredits else { continue }
for fraction in Self.backfillSampleFractions {
let sampledAt = start.addingTimeInterval(duration * fraction)
let recordKey = RecordKey(
resetsAt: reset,
sampledAt: sampledAt,
windowMinutes: windowMinutes,
accountKey: accountKey)
guard !existingRecordKeys.contains(recordKey) else { continue }
let cumulativeCredits = Self.creditsUsed(from: dayUsages, between: start, and: sampledAt)
let usedPercent = Self.clamp((cumulativeCredits / estimatedCreditsAtLimit) * 100, lower: 0, upper: 100)
synthesized.append(HistoricalUsageRecord(
v: Self.schemaVersion,
provider: .codex,
windowKind: .secondary,
source: .backfill,
accountKey: accountKey,
sampledAt: sampledAt,
usedPercent: usedPercent,
resetsAt: reset,
windowMinutes: windowMinutes))
existingRecordKeys.insert(recordKey)
}
}
guard !synthesized.isEmpty else { return existingDataset }
self.records.append(contentsOf: synthesized)
self.pruneOldRecords(now: now)
self.records.sort { lhs, rhs in
if lhs.sampledAt == rhs.sampledAt {
if lhs.resetsAt == rhs.resetsAt {
return lhs.usedPercent < rhs.usedPercent
}
return lhs.resetsAt < rhs.resetsAt
}
return lhs.sampledAt < rhs.sampledAt
}
self.persist()
return self.buildDataset(accountKey: accountKey)
}
private func shouldAccept(_ sample: HistoricalUsageRecord) -> Bool {
guard let prior = self.records
.last(where: {
$0.provider == sample.provider &&
$0.windowKind == sample.windowKind &&
$0.accountKey == sample.accountKey &&
$0.windowMinutes == sample.windowMinutes
})
else {
return true
}
if prior.resetsAt != sample.resetsAt { return true }
if sample.sampledAt.timeIntervalSince(prior.sampledAt) >= Self.writeInterval { return true }
if abs(sample.usedPercent - prior.usedPercent) >= Self.writeDeltaThreshold { return true }
return false
}
private func pruneOldRecords(now: Date) {
let cutoff = now.addingTimeInterval(-Self.retentionDays)
self.records.removeAll { $0.sampledAt < cutoff }
}
private func ensureLoaded() {
guard !self.loaded else { return }
self.loaded = true
self.records = self.readRecordsFromDisk()
self.pruneOldRecords(now: .init())
}
private func readRecordsFromDisk() -> [HistoricalUsageRecord] {
guard let data = try? Data(contentsOf: self.fileURL), !data.isEmpty else { return [] }
guard let text = String(data: data, encoding: .utf8) else { return [] }
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
var decoded: [HistoricalUsageRecord] = []
decoded.reserveCapacity(text.count / 80)
for rawLine in text.split(separator: "\n", omittingEmptySubsequences: true) {
let line = String(rawLine).trimmingCharacters(in: .whitespacesAndNewlines)
guard !line.isEmpty, let lineData = line.data(using: .utf8) else { continue }
guard var record = try? decoder.decode(HistoricalUsageRecord.self, from: lineData) else { continue }
record = HistoricalUsageRecord(
v: record.v,
provider: record.provider,
windowKind: record.windowKind,
source: record.source,
accountKey: record.accountKey?.isEmpty == false ? record.accountKey : nil,
sampledAt: record.sampledAt,
usedPercent: Self.clamp(record.usedPercent, lower: 0, upper: 100),
resetsAt: Self.normalizeReset(record.resetsAt),
windowMinutes: record.windowMinutes)
decoded.append(record)
}
return decoded
}
private func persist() {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = [.sortedKeys]
var lines: [String] = []
lines.reserveCapacity(self.records.count)
for record in self.records {
guard let data = try? encoder.encode(record),
let line = String(data: data, encoding: .utf8)
else {
continue
}
lines.append(line)
}
let payload = (lines.joined(separator: "\n") + "\n").data(using: .utf8) ?? Data()
let directory = self.fileURL.deletingLastPathComponent()
do {
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
try payload.write(to: self.fileURL, options: [.atomic])
} catch {
// Best-effort cache file; ignore write failures.
}
}
private func buildDataset(accountKey: String?) -> CodexHistoricalDataset? {
let scoped = self.records.filter { record in
guard Self.isCodexSecondaryRecord(record) else { return false }
if let accountKey {
return record.accountKey == accountKey
}
return record.accountKey == nil
}
return self.buildDataset(from: scoped)
}
private func buildDataset(
canonicalAccountKey: String?,
canonicalEmailHashKey: String?,
legacyEmailHash: String?,
hasAdjacentMultiAccountVeto: Bool) -> CodexHistoricalDataset?
{
guard let canonicalAccountKey else {
return self.buildDataset(accountKey: nil)
}
let shouldIncludeUnscoped = CodexHistoryOwnership.hasStrictSingleAccountContinuity(
scopedRawKeys: Self.scopedRawKeysRelevantToCodexUnscopedHistory(self.records),
targetCanonicalKey: canonicalAccountKey,
canonicalEmailHashKey: canonicalEmailHashKey,
legacyEmailHash: legacyEmailHash,
hasAdjacentMultiAccountVeto: hasAdjacentMultiAccountVeto)
let scoped = self.records.filter { record in
guard Self.isCodexSecondaryRecord(record) else { return false }
guard let rawKey = record.accountKey else {
return shouldIncludeUnscoped
}
let owner = CodexHistoryOwnership.classifyPersistedKey(rawKey, legacyEmailHash: legacyEmailHash)
return CodexHistoryOwnership.belongsToTargetContinuity(
owner,
targetCanonicalKey: canonicalAccountKey,
canonicalEmailHashKey: canonicalEmailHashKey)
}
return self.buildDataset(from: scoped)
}
private func buildDataset(from scoped: [HistoricalUsageRecord]) -> CodexHistoricalDataset? {
struct WeekKey: Hashable {
let resetsAt: Date
let windowMinutes: Int
}
if scoped.isEmpty { return nil }
let grouped = Dictionary(grouping: scoped) {
WeekKey(resetsAt: $0.resetsAt, windowMinutes: $0.windowMinutes)
}
var weeks: [HistoricalWeekProfile] = []
weeks.reserveCapacity(grouped.count)
for (key, samples) in grouped {
let duration = TimeInterval(key.windowMinutes) * 60
guard duration > 0 else { continue }
let windowStart = key.resetsAt.addingTimeInterval(-duration)
guard Self.isCompleteWeek(samples: samples, windowStart: windowStart, resetsAt: key.resetsAt) else {
continue
}
guard let curve = Self.reconstructWeekCurve(
samples: samples,
windowStart: windowStart,
windowDuration: duration,
gridPointCount: CodexHistoricalDataset.gridPointCount)
else {
continue
}
weeks.append(HistoricalWeekProfile(
resetsAt: key.resetsAt,
windowMinutes: key.windowMinutes,
curve: curve))
}
weeks.sort { $0.resetsAt < $1.resetsAt }
if weeks.isEmpty { return nil }
return CodexHistoricalDataset(weeks: weeks)
}
private nonisolated static func isCodexSecondaryRecord(_ record: HistoricalUsageRecord) -> Bool {
record.provider == .codex && record.windowKind == .secondary && record.windowMinutes > 0
}
private static func reconstructWeekCurve(
samples: [HistoricalUsageRecord],
windowStart: Date,
windowDuration: TimeInterval,
gridPointCount: Int) -> [Double]?
{
guard gridPointCount >= 2 else { return nil }
var points = samples.map { sample -> (u: Double, value: Double) in
let offset = sample.sampledAt.timeIntervalSince(windowStart)
let u = Self.clamp(offset / windowDuration, lower: 0, upper: 1)
return (u: u, value: Self.clamp(sample.usedPercent, lower: 0, upper: 100))
}
points.sort { lhs, rhs in
if lhs.u == rhs.u {
return lhs.value < rhs.value
}
return lhs.u < rhs.u
}
guard !points.isEmpty else { return nil }
// Enforce monotonicity on observed samples before interpolation.
var monotonePoints: [(u: Double, value: Double)] = []
monotonePoints.reserveCapacity(points.count)
var runningMax = 0.0
for point in points {
runningMax = max(runningMax, point.value)
monotonePoints.append((u: point.u, value: runningMax))
}
// Anchor reconstructed curves to reset start and end-of-week plateau.
let endValue = monotonePoints.last?.value ?? 0
monotonePoints.append((u: 0, value: 0))
monotonePoints.append((u: 1, value: endValue))
monotonePoints.sort { lhs, rhs in
if lhs.u == rhs.u {
return lhs.value < rhs.value
}
return lhs.u < rhs.u
}
runningMax = 0
for index in monotonePoints.indices {
runningMax = max(runningMax, monotonePoints[index].value)
monotonePoints[index].value = runningMax
}
var curve = Array(repeating: 0.0, count: gridPointCount)
let first = monotonePoints[0]
let last = monotonePoints[monotonePoints.count - 1]
var upperIndex = 1
let denominator = Double(gridPointCount - 1)
for index in 0..<gridPointCount {
let u = Double(index) / denominator
if u <= first.u {
curve[index] = first.value
continue
}
if u >= last.u {
curve[index] = last.value
continue
}
while upperIndex < monotonePoints.count, monotonePoints[upperIndex].u < u {
upperIndex += 1
}
let hi = monotonePoints[min(upperIndex, monotonePoints.count - 1)]
let lo = monotonePoints[max(0, upperIndex - 1)]
if hi.u <= lo.u {
curve[index] = max(lo.value, hi.value)
continue
}
let ratio = Self.clamp((u - lo.u) / (hi.u - lo.u), lower: 0, upper: 1)
curve[index] = lo.value + (hi.value - lo.value) * ratio
}
// Re-enforce monotonicity on reconstructed grid.
var curveMax = 0.0
for index in curve.indices {
curve[index] = Self.clamp(curve[index], lower: 0, upper: 100)
curveMax = max(curveMax, curve[index])
curve[index] = curveMax
}
return curve
}
private static func isCompleteWeek(samples: [HistoricalUsageRecord], windowStart: Date, resetsAt: Date) -> Bool {
guard samples.count >= self.minimumWeekSamples else { return false }
let startBoundary = windowStart.addingTimeInterval(Self.boundaryCoverageWindow)
let endBoundary = resetsAt.addingTimeInterval(-Self.boundaryCoverageWindow)
let hasStartCoverage = samples.contains { sample in
sample.sampledAt >= windowStart && sample.sampledAt <= startBoundary
}
let hasEndCoverage = samples.contains { sample in
sample.sampledAt >= endBoundary && sample.sampledAt <= resetsAt
}
return hasStartCoverage && hasEndCoverage
}
private nonisolated static func scopedRawKeysRelevantToCodexUnscopedHistory(
_ records: [HistoricalUsageRecord]) -> [String]
{
let unscopedRecords = records.filter { record in
Self.isCodexSecondaryRecord(record) && record.accountKey == nil
}
guard let continuityWindow = self.historicalContinuityWindow(for: unscopedRecords) else {
return []
}
return records.compactMap { record in
guard Self.isCodexSecondaryRecord(record),
let accountKey = record.accountKey,
continuityWindow.contains(record.sampledAt)
else {
return nil
}
return accountKey
}
}
private nonisolated static func historicalContinuityWindow(
for records: [HistoricalUsageRecord]) -> ClosedRange<Date>?
{
let sampledDates = records.map(\.sampledAt)
guard let lowerBound = sampledDates.min(),
let upperBound = sampledDates.max()
else {
return nil
}
let expansion = TimeInterval(records.map(\.windowMinutes).max() ?? 0) * 60
return lowerBound.addingTimeInterval(-expansion)...upperBound.addingTimeInterval(expansion)
}
private struct DayUsage {
let start: Date
let end: Date
let creditsUsed: Double
}
private static func parseDayUsages(
from breakdown: [OpenAIDashboardDailyBreakdown],
asOf: Date,
fillingFrom expectedCoverageStart: Date? = nil) -> [DayUsage]
{
var creditsByStart: [Date: Double] = [:]
creditsByStart.reserveCapacity(breakdown.count)
for day in breakdown {
guard let dayStart = Self.dayStart(for: day.day) else { continue }
creditsByStart[dayStart, default: 0] += max(0, day.totalCreditsUsed)
}
let calendar = Self.gregorianCalendar()
var dayUsages: [DayUsage] = []
dayUsages.reserveCapacity(creditsByStart.count)
for (dayStart, credits) in creditsByStart {
guard let nominalEnd = calendar.date(byAdding: .day, value: 1, to: dayStart) else { continue }
let effectiveEnd: Date = if dayStart <= asOf, asOf < nominalEnd {
asOf
} else {
nominalEnd
}
guard effectiveEnd > dayStart else { continue }
dayUsages.append(DayUsage(start: dayStart, end: effectiveEnd, creditsUsed: credits))
}
dayUsages.sort { lhs, rhs in lhs.start < rhs.start }
return Self.fillMissingZeroUsageDays(
in: dayUsages,
through: asOf,
fillingFrom: expectedCoverageStart)
}
private static func fillMissingZeroUsageDays(
in dayUsages: [DayUsage],
through asOf: Date,
fillingFrom expectedCoverageStart: Date? = nil) -> [DayUsage]
{
guard let firstStart = dayUsages.first?.start else { return [] }
let calendar = Self.gregorianCalendar()
let fillStart: Date = if let expectedCoverageStart {
min(firstStart, calendar.startOfDay(for: expectedCoverageStart))
} else {
firstStart
}
let finalDayStart = calendar.startOfDay(for: asOf)
guard fillStart <= finalDayStart else { return dayUsages }
let creditsByStart = Dictionary(uniqueKeysWithValues: dayUsages.map { ($0.start, $0.creditsUsed) })
let daySpan = max(0, calendar.dateComponents([.day], from: fillStart, to: finalDayStart).day ?? 0)
var filled: [DayUsage] = []
filled.reserveCapacity(daySpan + 1)
var cursor = fillStart
while cursor <= finalDayStart {
guard let nominalEnd = calendar.date(byAdding: .day, value: 1, to: cursor) else { break }
let effectiveEnd: Date = if cursor <= asOf, asOf < nominalEnd {
asOf
} else {
nominalEnd
}
guard effectiveEnd > cursor else { break }
filled.append(DayUsage(
start: cursor,
end: effectiveEnd,
creditsUsed: creditsByStart[cursor] ?? 0))
guard let next = calendar.date(byAdding: .day, value: 1, to: cursor) else { break }
cursor = next
}
return filled
}
private static func dayStart(for key: String) -> Date? {
let components = key.split(separator: "-", omittingEmptySubsequences: true)
guard components.count == 3,
let year = Int(components[0]),
let month = Int(components[1]),
let day = Int(components[2])
else {
return nil
}
let calendar = Self.gregorianCalendar()
var dateComponents = DateComponents()
dateComponents.calendar = calendar
dateComponents.timeZone = calendar.timeZone
dateComponents.year = year
dateComponents.month = month
dateComponents.day = day
dateComponents.hour = 0
dateComponents.minute = 0
dateComponents.second = 0
return dateComponents.date
}
private static func creditsUsed(from dayUsages: [DayUsage], between start: Date, and end: Date) -> Double {
guard end > start else { return 0 }
var total = 0.0
for day in dayUsages {
if day.end <= start { continue }
if day.start >= end { break }
let overlapStart = max(day.start, start)
let overlapEnd = min(day.end, end)
guard overlapEnd > overlapStart else { continue }
let dayDuration = day.end.timeIntervalSince(day.start)
guard dayDuration > 0 else { continue }
let overlap = overlapEnd.timeIntervalSince(overlapStart)
total += day.creditsUsed * (overlap / dayDuration)
}
return max(0, total)
}
nonisolated static func defaultFileURL() -> URL {
let root = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
?? FileManager.default.homeDirectoryForCurrentUser
return root
.appendingPathComponent("CodexBar", isDirectory: true)
.appendingPathComponent("usage-history.jsonl", isDirectory: false)
}
private nonisolated static func clamp(_ value: Double, lower: Double, upper: Double) -> Double {
min(upper, max(lower, value))
}
private nonisolated static func clampDate(_ value: Date, lower: Date, upper: Date) -> Date {
min(upper, max(lower, value))
}
private nonisolated static func normalizeReset(_ value: Date) -> Date {
let bucket = Self.resetBucketSeconds
guard bucket > 0 else { return value }
let rounded = (value.timeIntervalSinceReferenceDate / bucket).rounded() * bucket
return Date(timeIntervalSinceReferenceDate: rounded)
}
private nonisolated static func gregorianCalendar() -> Calendar {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone.current
return calendar
}
#if DEBUG
nonisolated static func _dayStartForTesting(_ key: String) -> Date? {
self.dayStart(for: key)
}
nonisolated static func _creditsUsedForTesting(
breakdown: [OpenAIDashboardDailyBreakdown],
asOf: Date,
start: Date,
end: Date) -> Double
{
let dayUsages = Self.parseDayUsages(from: breakdown, asOf: asOf)
return Self.creditsUsed(from: dayUsages, between: start, and: end)
}
#endif
}
enum CodexHistoricalPaceEvaluator {
static let minimumCompleteWeeksForHistorical = 3
static let minimumWeeksForRisk = 5
private static let recencyTauWeeks: Double = 3
private static let epsilon: Double = 1e-9
private static let resetBucketSeconds: TimeInterval = 5 * 60
static func evaluate(window: RateWindow, now: Date, dataset: CodexHistoricalDataset?) -> UsagePace? {
guard let dataset else { return nil }
guard let resetsAt = window.resetsAt else { return nil }
let minutes = window.windowMinutes ?? 10080
guard minutes > 0 else { return nil }
let duration = TimeInterval(minutes) * 60
guard duration > 0 else { return nil }
let timeUntilReset = resetsAt.timeIntervalSince(now)
guard timeUntilReset > 0, timeUntilReset <= duration else { return nil }
let normalizedResetsAt = Self.normalizeReset(resetsAt)
let elapsed = Self.clamp(duration - timeUntilReset, lower: 0, upper: duration)
let actual = Self.clamp(window.usedPercent, lower: 0, upper: 100)
if elapsed == 0, actual > 0 { return nil }
let uNow = Self.clamp(elapsed / duration, lower: 0, upper: 1)
let scopedWeeks = dataset.weeks.filter { week in
week.windowMinutes == minutes && week.resetsAt < normalizedResetsAt
}
guard scopedWeeks.count >= Self.minimumCompleteWeeksForHistorical else { return nil }
let weightedWeeks = scopedWeeks.map { week in
let ageWeeks = Self.clamp(
normalizedResetsAt.timeIntervalSince(week.resetsAt) / duration,
lower: 0,
upper: Double.greatestFiniteMagnitude)
let weight = exp(-ageWeeks / Self.recencyTauWeeks)
return (week: week, weight: weight)
}
let totalWeight = weightedWeeks.reduce(0.0) { $0 + $1.weight }
guard totalWeight > Self.epsilon else { return nil }
let totalWeightSquared = weightedWeeks.reduce(0.0) { $0 + ($1.weight * $1.weight) }
let nEff = totalWeightSquared > Self.epsilon ? (totalWeight * totalWeight) / totalWeightSquared : 0
let lambda = Self.clamp((nEff - 2) / 6, lower: 0, upper: 1)
let gridCount = CodexHistoricalDataset.gridPointCount
let denominator = Double(gridCount - 1)
var expectedCurve = Array(repeating: 0.0, count: gridCount)
for index in 0..<gridCount {
let u = Double(index) / denominator
let values = weightedWeeks.map { $0.week.curve[index] }
let weights = weightedWeeks.map(\.weight)
let historicalMedian = Self.weightedMedian(values: values, weights: weights)
let linearBaseline = 100 * u
// Historical demand can exceed a sustainable quota pace. Never call that excess a reserve.
expectedCurve[index] = Self.clamp(
(lambda * historicalMedian) + ((1 - lambda) * linearBaseline),
lower: 0,
upper: linearBaseline)
}
// Expected cumulative usage should be monotone.
var runningExpected = 0.0
for index in expectedCurve.indices {
runningExpected = max(runningExpected, expectedCurve[index])
expectedCurve[index] = runningExpected
}
let expectedNow = Self.interpolate(curve: expectedCurve, at: uNow)
var weightedRunOutMass = 0.0
var crossingCandidates: [(etaSeconds: TimeInterval, weight: Double)] = []
crossingCandidates.reserveCapacity(weightedWeeks.count)
for weighted in weightedWeeks {
var extendedCurve = weighted.week.curve
if let capIndex = extendedCurve.firstIndex(where: { $0 >= 100 - Self.epsilon }),
capIndex > 0, capIndex < extendedCurve.count - 1
{
let gridCount = CodexHistoricalDataset.gridPointCount
let uCap = Double(capIndex) / Double(gridCount - 1)
let valCap = extendedCurve[capIndex]
let slope: Double = valCap / uCap
for i in capIndex..<extendedCurve.count {
let u = Double(i) / Double(gridCount - 1)
extendedCurve[i] = slope * u
}
}
let weight = weighted.weight
let weekNow = Self.interpolate(curve: extendedCurve, at: uNow)
let shift = actual - weekNow
let shiftedEnd = (extendedCurve.last ?? 0) + shift
let runOut = shiftedEnd >= 100 - Self.epsilon
if runOut {
weightedRunOutMass += weight
if let crossingU = Self.firstCrossing(
after: uNow,
curve: extendedCurve,
shift: shift,
actualAtNow: actual)
{
let etaSeconds = max(0, (crossingU - uNow) * duration)
crossingCandidates.append((etaSeconds: etaSeconds, weight: weight))
}
}
}
let smoothedProbability = Self.clamp(
(weightedRunOutMass + 0.5) / (totalWeight + 1),
lower: 0,
upper: 1)
var runOutProbability: Double? = scopedWeeks.count >= Self.minimumWeeksForRisk ? smoothedProbability : nil
var willLastToReset = smoothedProbability < 0.5
var etaSeconds: TimeInterval?
if actual >= 100 {
willLastToReset = false
etaSeconds = 0
runOutProbability = 1
} else if !willLastToReset {
let values = crossingCandidates.map(\.etaSeconds)
let weights = crossingCandidates.map(\.weight)
if values.isEmpty {
willLastToReset = true
} else {
etaSeconds = max(0, Self.weightedMedian(values: values, weights: weights))
}
}
return UsagePace.historical(
expectedUsedPercent: expectedNow,
actualUsedPercent: actual,
etaSeconds: etaSeconds,
willLastToReset: willLastToReset,
runOutProbability: runOutProbability,
projectedRemainingUsage: max(0, (expectedCurve.last ?? expectedNow) - expectedNow))
}
private static func firstCrossing(
after uNow: Double,
curve: [Double],
shift: Double,
actualAtNow: Double) -> Double?
{
let gridCount = curve.count
guard gridCount >= 2 else { return nil }
let denominator = Double(gridCount - 1)
var previousU = uNow
var previousValue = actualAtNow
let startIndex = min(gridCount - 1, max(1, Int(floor(uNow * denominator)) + 1))
for index in startIndex..<gridCount {
let u = Double(index) / denominator
if u <= uNow + Self.epsilon { continue }
let value = Self.clamp(curve[index] + shift, lower: 0, upper: 100)
if previousValue < 100 - Self.epsilon, value >= 100 - Self.epsilon {
let delta = value - previousValue
if abs(delta) <= Self.epsilon { return u }
let ratio = Self.clamp((100 - previousValue) / delta, lower: 0, upper: 1)
return Self.clamp(previousU + ratio * (u - previousU), lower: uNow, upper: 1)
}
previousU = u
previousValue = value
}
return nil
}
private static func interpolate(curve: [Double], at u: Double) -> Double {
guard !curve.isEmpty else { return 0 }
if curve.count == 1 { return curve[0] }
let clipped = Self.clamp(u, lower: 0, upper: 1)
let scaled = clipped * Double(curve.count - 1)
let lower = Int(floor(scaled))
let upper = min(curve.count - 1, lower + 1)
if lower == upper { return curve[lower] }
let ratio = scaled - Double(lower)
return curve[lower] + ((curve[upper] - curve[lower]) * ratio)
}
private static func weightedMedian(values: [Double], weights: [Double]) -> Double {
guard values.count == weights.count, !values.isEmpty else { return 0 }
let pairs = zip(values, weights)
.map { (value: $0, weight: max(0, $1)) }
.sorted { lhs, rhs in lhs.value < rhs.value }
let totalWeight = pairs.reduce(0.0) { $0 + $1.weight }
if totalWeight <= Self.epsilon {
let sortedValues = values.sorted()
return sortedValues[sortedValues.count / 2]
}
let threshold = totalWeight / 2
var cumulative = 0.0
for pair in pairs {
cumulative += pair.weight
if cumulative >= threshold {
return pair.value
}
}
return pairs.last?.value ?? 0
}
private static func clamp(_ value: Double, lower: Double, upper: Double) -> Double {
min(upper, max(lower, value))
}
private static func normalizeReset(_ value: Date) -> Date {
let bucket = Self.resetBucketSeconds
guard bucket > 0 else { return value }
let rounded = (value.timeIntervalSinceReferenceDate / bucket).rounded() * bucket
return Date(timeIntervalSinceReferenceDate: rounded)
}
}
@@ -0,0 +1,152 @@
import CodexBarCore
import Foundation
enum IconRemainingResolver {
private static let visibleZeroPercent = 0.0001
private static let antigravityQuotaSummaryWindowIDPrefix = "antigravity-quota-summary-"
// Antigravity quota summaries expose exact 5-hour session and weekly buckets for the compact icon.
private static let sessionWindowMinutes = 5 * 60
private static let weeklyWindowMinutes = 7 * 24 * 60
private static func codexProjection(snapshot: UsageSnapshot, now: Date) -> CodexConsumerProjection {
CodexConsumerProjection.make(
surface: .menuBar,
context: CodexConsumerProjection.Context(
snapshot: snapshot,
rawUsageError: nil,
liveCredits: nil,
rawCreditsError: nil,
liveDashboard: nil,
rawDashboardError: nil,
dashboardAttachmentAuthorized: false,
dashboardRequiresLogin: false,
now: now))
}
private static func codexVisibleWindows(snapshot: UsageSnapshot, now: Date) -> [RateWindow] {
let projection = self.codexProjection(snapshot: snapshot, now: now)
return projection.visibleRateLanes.compactMap { projection.menuBarSelectableRateWindow(for: $0) }
}
private static func antigravityQuotaSummaryWindows(
snapshot: UsageSnapshot)
-> (primary: RateWindow?, secondary: RateWindow?)?
{
let quotaSummaryWindows = snapshot.extraRateWindows?
.filter {
$0.id.hasPrefix(Self.antigravityQuotaSummaryWindowIDPrefix)
} ?? []
guard !quotaSummaryWindows.isEmpty else { return nil }
return self.antigravityQuotaSummaryPair(in: quotaSummaryWindows.filter(\.usageKnown))
}
private static func antigravityQuotaSummaryPair(
in windows: [NamedRateWindow])
-> (primary: RateWindow?, secondary: RateWindow?)?
{
let session = self.mostConstrainedWindow(in: windows, windowMinutes: Self.sessionWindowMinutes)
let weekly = self.mostConstrainedWindow(in: windows, windowMinutes: Self.weeklyWindowMinutes)
guard session != nil || weekly != nil else { return nil }
return (primary: session, secondary: weekly)
}
/// Returns the highest-usage window for an exact Antigravity compact-icon cadence.
private static func mostConstrainedWindow(in windows: [NamedRateWindow], windowMinutes: Int) -> RateWindow? {
windows
.filter { $0.window.windowMinutes == windowMinutes }
.max { lhs, rhs in
if lhs.window.usedPercent != rhs.window.usedPercent {
return lhs.window.usedPercent < rhs.window.usedPercent
}
// max(by:) keeps the right-hand element when this returns true; use `>` so the smallest id wins ties.
return lhs.id > rhs.id
}?
.window
}
static func resolvedWindows(
snapshot: UsageSnapshot,
style: IconStyle,
secondaryOverrideWindowID: String? = nil,
now: Date = Date())
-> (primary: RateWindow?, secondary: RateWindow?)
{
if style == .perplexity {
let windows = snapshot.orderedPerplexityDisplayWindows()
return (
primary: windows.first,
secondary: windows.dropFirst().first)
}
if style == .antigravity {
// Only current quota-summary buckets define the fixed session/weekly icon lanes.
return self.antigravityQuotaSummaryWindows(snapshot: snapshot)
?? (primary: nil, secondary: nil)
}
if style == .codex {
let windows = self.codexVisibleWindows(snapshot: snapshot, now: now)
return (
primary: windows.first,
secondary: windows.dropFirst().first)
}
if style == .copilot,
let secondaryOverrideWindowID,
let extraWindow = snapshot.extraRateWindows?.first(where: { $0.id == secondaryOverrideWindowID })?.window
{
return (
primary: snapshot.primary,
secondary: extraWindow)
}
return (
primary: snapshot.primary,
secondary: snapshot.secondary)
}
static func resolvedRemaining(
snapshot: UsageSnapshot,
style: IconStyle,
secondaryOverrideWindowID: String? = nil,
now: Date = Date())
-> (primary: Double?, secondary: Double?)
{
let windows = self.resolvedWindows(
snapshot: snapshot,
style: style,
secondaryOverrideWindowID: secondaryOverrideWindowID,
now: now)
return (
primary: windows.primary?.remainingPercent,
secondary: windows.secondary?.remainingPercent)
}
static func resolvedPercents(
snapshot: UsageSnapshot,
style: IconStyle,
showUsed: Bool,
renderingStyle: IconStyle? = nil,
secondaryOverrideWindowID: String? = nil,
now: Date = Date())
-> (primary: Double?, secondary: Double?)
{
let windows = Self.resolvedWindows(
snapshot: snapshot,
style: style,
secondaryOverrideWindowID: secondaryOverrideWindowID,
now: now)
var percents = (
primary: showUsed ? windows.primary?.usedPercent : windows.primary?.remainingPercent,
secondary: showUsed ? windows.secondary?.usedPercent : windows.secondary?.remainingPercent)
// Provider style chooses the usage lanes; rendering style controls renderer-specific layout sentinels.
// Merged icons still resolve Warp's lanes, but render as `.combined` and must keep the real percentage.
if showUsed, style == .warp, (renderingStyle ?? style) == .warp, let secondary = windows.secondary {
if secondary.remainingPercent <= 0 {
// Preserve Warp's exhausted/no-bonus layout even though used percent is 100.
percents.secondary = 0
} else if percents.secondary == 0 {
// A zero fill means "lane absent" to IconRenderer; keep an unused bonus lane visible.
percents.secondary = self.visibleZeroPercent
}
}
return percents
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,914 @@
import CodexBarCore
import SwiftUI
struct InlineUsageDashboardModel: Equatable {
struct KPI: Equatable {
let title: String
let value: String
let emphasis: Bool
}
struct Point: Equatable, Identifiable {
let id: String
let label: String
let value: Double
let accessibilityValue: String
}
enum ValueStyle: Equatable {
case currencyUSD
case currency(symbol: String)
case tokens
case points
}
let accessibilityLabel: String
let valueStyle: ValueStyle
let kpis: [KPI]
let points: [Point]
let detailLines: [String]
/// Provider branding color used to fill the mini usage bars. When nil the bars fall back to a
/// neutral palette derived from `valueStyle`.
var barColor: Color?
/// ISO 4217 currency code for cost dashboards. When non-nil, `MiniUsageBars` shows a max-cost scale label.
/// Nil for token/points dashboards.
var currencyCode: String?
}
extension UsageMenuCardView.Model {
static func apiProviderUsageNotes(input: Input) -> [String]? {
if input.provider == .openai,
let usage = input.snapshot?.openAIAPIUsage
{
return self.openAIAPIUsageNotes(usage)
}
if input.provider == .deepgram,
let usage = input.snapshot?.deepgramUsage
{
return usage.displayLines
}
if input.provider == .clawrouter,
let usage = input.snapshot?.clawRouterUsage
{
var notes = [
"\(UsageFormatter.tokenCountString(usage.requestCount)) \(L("requests")) · " +
"\(UsageFormatter.tokenCountString(usage.totalTokens)) \(L("tokens"))",
]
if usage.errorCount > 0 {
notes.append("\(usage.successCount) succeeded · \(usage.errorCount) failed")
}
if !usage.providers.isEmpty {
let mix = usage.providers.prefix(5)
.map { "\($0.provider): \(UsageFormatter.tokenCountString($0.requestCount))" }
.joined(separator: " · ")
notes.append("Routed providers: \(mix)")
}
return notes
}
if input.provider == .wayfinder,
let usage = input.snapshot?.wayfinderUsage
{
return usage.displayLines
}
if input.provider == .minimax,
input.showOptionalCreditsAndExtraUsage,
let billing = input.snapshot?.minimaxUsage?.billingSummary
{
return [
String(format: L("Today: %@ tokens"), UsageFormatter.tokenCountString(billing.todayTokens)),
String(
format: L("Last 30 days: %@ tokens"),
UsageFormatter.tokenCountString(billing.last30DaysTokens)),
]
}
if input.provider == .deepseek,
input.showOptionalCreditsAndExtraUsage,
let usage = input.snapshot?.deepseekUsage
{
let symbol = usage.currency == "CNY" ? "¥" : "$"
let todayCostStr = usage.todayCost.map { "\(symbol)\(String(format: "%.4f", max(0, $0)))" } ?? ""
return [
String(
format: L("Today: %@ · %@ tokens"),
todayCostStr,
UsageFormatter.tokenCountString(usage.todayTokens)),
String(format: L("This month: %@ tokens"), UsageFormatter.tokenCountString(usage.currentMonthTokens)),
]
}
if input.provider == .poe,
let usage = input.snapshot?.poeUsage
{
return self.poeUsageNotes(usage, now: input.now)
}
if input.provider == .ollama,
input.snapshot?.identity?.loginMethod == "API key"
{
return [L("API key verified. Ollama does not expose Cloud quota limits through the API.")]
}
return nil
}
static func openAIAPIUsageNotes(_ usage: OpenAIAPIUsageSnapshot) -> [String] {
let today = usage.currentDay
let seven = usage.last7Days
let thirty = usage.last30Days
let historyLabel = usage.historyWindowLabel
let todayNote = String(
format: L("Today: %@ · %@ tokens"),
UsageFormatter.usdString(today.costUSD),
UsageFormatter.tokenCountString(today.totalTokens))
let sevenDayNote = "7d: \(UsageFormatter.usdString(seven.costUSD)) · " +
"\(UsageFormatter.tokenCountString(seven.requests)) \(L("requests"))"
let thirtyDayNote =
"\(historyLabel): \(UsageFormatter.tokenCountString(thirty.totalTokens)) \(L("tokens")) · " +
"\(UsageFormatter.tokenCountString(thirty.requests)) \(L("requests"))"
var notes: [String] = [
todayNote,
sevenDayNote,
thirtyDayNote,
]
if let topModel = usage.topModels.first {
notes.append("\(L("Top model")): \(topModel.name)")
}
return notes
}
static func poeUsageNotes(
_ usage: PoeUsageHistorySnapshot,
now: Date = Date(),
calendar: Calendar = .current) -> [String]
{
let today = usage.currentDay(now: now, calendar: calendar)
let week = usage.last7Days
let month = usage.last30Days
let todayUSD = today.costUSD.map { " · \(UsageFormatter.usdString($0))" } ?? ""
let weekUSD = week.costUSD.map { " · \(UsageFormatter.usdString($0))" } ?? ""
let monthUSD = month.costUSD.map { " · \(UsageFormatter.usdString($0))" } ?? ""
let todayLine = "Today: \(Self.pointsSummary(today.points)) · " +
"\(UsageFormatter.tokenCountString(today.requests)) \(L("requests"))\(todayUSD)"
let weekLine = "7d: \(Self.pointsSummary(week.points)) · " +
"\(UsageFormatter.tokenCountString(week.requests)) \(L("requests"))\(weekUSD)"
let monthLine = "30d: \(Self.pointsSummary(month.points)) · " +
"\(UsageFormatter.tokenCountString(month.requests)) \(L("requests"))\(monthUSD)"
var notes = [
todayLine,
weekLine,
monthLine,
]
if let topModel = usage.topModels.first {
notes.append("\(L("Top model")): \(topModel.name) (\(Self.pointsSummary(topModel.points)))")
}
if !usage.topUsageTypes.isEmpty {
let mix = usage.topUsageTypes.prefix(2)
.map { "\($0.name): \(Self.pointsSummary($0.points))" }
.joined(separator: " · ")
notes.append("Usage mix: \(mix)")
}
return notes
}
static func inlineUsageDashboard(input: Input) -> InlineUsageDashboardModel? {
guard var model = self.resolveInlineUsageDashboard(input: input) else { return nil }
model.barColor = Self.inlineDashboardBarColor(for: input.provider)
return model
}
/// Provider branding color for the inline usage bars, matching the provider's switcher tab and
/// detailed cost-history chart.
static func inlineDashboardBarColor(for provider: UsageProvider) -> Color {
let color = ProviderDescriptorRegistry.descriptor(for: provider).branding.color
return Color(red: color.red, green: color.green, blue: color.blue)
}
private static func resolveInlineUsageDashboard(input: Input) -> InlineUsageDashboardModel? {
if self.usesProviderCostHistoryAsPrimaryDashboard(input.provider),
let tokenSnapshot = primaryCostHistorySnapshot(input: input),
!tokenSnapshot.daily.isEmpty
{
return self.costHistoryInlineDashboard(
provider: input.provider,
snapshot: tokenSnapshot,
comparisonPeriodsEnabled: input.costComparisonPeriodsEnabled)
}
if input.provider == .claude,
let usage = input.snapshot?.claudeAdminAPIUsage
{
return Self.claudeAdminAPIInlineDashboard(usage)
}
if input.provider == .openrouter,
let usage = input.snapshot?.openRouterUsage
{
return Self.openRouterInlineDashboard(usage)
}
if input.provider == .crossmodel,
let usage = input.snapshot?.crossModelUsage
{
return Self.crossModelInlineDashboard(usage)
}
if input.provider == .zai,
let modelUsage = input.snapshot?.zaiUsage?.modelUsage
{
return Self.zaiInlineDashboard(modelUsage: modelUsage, now: input.now)
}
if input.provider == .minimax,
input.showOptionalCreditsAndExtraUsage,
let billing = input.snapshot?.minimaxUsage?.billingSummary,
!billing.daily.isEmpty
{
return Self.minimaxInlineDashboard(billing)
}
if input.provider == .deepseek,
input.showOptionalCreditsAndExtraUsage,
let usage = input.snapshot?.deepseekUsage,
!usage.daily.isEmpty
{
return Self.deepseekInlineDashboard(usage)
}
if input.provider == .poe,
let usage = input.snapshot?.poeUsage,
!usage.daily.isEmpty
{
return Self.poeInlineDashboard(usage, now: input.now)
}
if [.codex, .claude, .vertexai, .bedrock].contains(input.provider),
input.tokenCostInlineDashboardEnabled,
let tokenSnapshot = input.tokenSnapshot,
!tokenSnapshot.daily.isEmpty
{
return Self.costHistoryInlineDashboard(
provider: input.provider,
snapshot: tokenSnapshot,
comparisonPeriodsEnabled: input.costComparisonPeriodsEnabled)
}
return nil
}
static func usesProviderCostHistoryAsPrimaryDashboard(_ provider: UsageProvider) -> Bool {
provider == .openai || provider == .mistral
}
static func primaryCostHistorySnapshot(input: Input) -> CostUsageTokenSnapshot? {
switch input.provider {
case .openai:
if let projected = input.snapshot?.openAIAPIUsage?.toCostUsageTokenSnapshot() {
return projected
}
return input.snapshot == nil ? input.tokenSnapshot : nil
case .mistral:
if let projected = input.snapshot?.mistralUsage?.toCostUsageTokenSnapshot() {
return projected
}
return input.snapshot == nil ? input.tokenSnapshot : nil
default:
return input.tokenSnapshot
}
}
static func poeInlineDashboard(
_ usage: PoeUsageHistorySnapshot,
now: Date = Date(),
calendar: Calendar = .current) -> InlineUsageDashboardModel
{
let today = usage.currentDay(now: now, calendar: calendar)
let week = usage.last7Days
let month = usage.last30Days
let points = usage.daily.suffix(30).map {
InlineUsageDashboardModel.Point(
id: $0.day,
label: Self.shortDayLabel($0.day),
value: $0.points,
accessibilityValue: "\($0.day): \(Self.pointsSummary($0.points))")
}
var details = ["30d requests: \(UsageFormatter.tokenCountString(month.requests))"]
if let topModel = usage.topModel {
details.append("\(L("Top model")): \(topModel)")
}
if !usage.topUsageTypes.isEmpty {
let mix = usage.topUsageTypes.prefix(3)
.map { "\($0.name): \(Self.pointsSummary($0.points))" }
.joined(separator: " · ")
details.append("Usage mix: \(mix)")
}
if let usd = today.costUSD, usd > 0 {
details.append("Today USD: \(UsageFormatter.usdString(usd))")
}
if let usd = week.costUSD, usd > 0 {
details.append("7d USD: \(UsageFormatter.usdString(usd))")
}
if let usd = month.costUSD, usd > 0 {
details.append("30d USD: \(UsageFormatter.usdString(usd))")
}
let recent = usage.recentEntries(limit: 2)
if !recent.isEmpty {
let text = recent.map { "\($0.model) \(Self.pointsSummary($0.points))" }.joined(separator: " · ")
details.append("Recent: \(text)")
}
return InlineUsageDashboardModel(
accessibilityLabel: "Poe points usage trend",
valueStyle: .points,
kpis: [
.init(title: L("Today"), value: Self.pointsSummary(today.points), emphasis: true),
.init(title: "7d", value: Self.pointsSummary(week.points), emphasis: false),
.init(title: "30d", value: Self.pointsSummary(month.points), emphasis: false),
.init(title: L("Requests"), value: UsageFormatter.tokenCountString(month.requests), emphasis: false),
],
points: points,
detailLines: details)
}
static func pointsSummary(_ value: Double) -> String {
let clamped = max(0, value)
if clamped.rounded() == clamped {
return "\(UsageFormatter.tokenCountString(Int(clamped))) points"
}
return "\(String(format: "%.1f", clamped)) points"
}
private static func costHistoryInlineDashboard(
provider: UsageProvider,
snapshot: CostUsageTokenSnapshot,
comparisonPeriodsEnabled: Bool) -> InlineUsageDashboardModel
{
let historyDays = max(1, min(365, snapshot.historyDays))
let historyTitle = snapshot.historyLabel
?? (historyDays == 1
? L("Today")
: historyDays == 30
? L("30d cost")
: "\(String(format: L("Last %d days"), historyDays)) \(L("Cost"))")
let tokenHistoryTitle = snapshot.historyLabel.map { "\($0) \(L("tokens"))" }
?? (historyDays == 1
? L("Today tokens")
: historyDays == 30
? L("30d tokens")
: String(format: L("%@ tokens"), String(format: L("Last %d days"), historyDays)))
let requestHistoryTitle = snapshot.historyLabel.map { "\($0) \(L("requests"))" }
?? (historyDays == 1
? L("Today requests")
: historyDays == 30
? L("30d requests")
: String(format: L("%@ requests"), String(format: L("Last %d days"), historyDays)))
let periodLabel = snapshot.historyLabel?.lowercased()
?? (historyDays == 1 ? "today" : "\(historyDays) day")
let points = snapshot.daily.suffix(historyDays).compactMap { entry -> InlineUsageDashboardModel.Point? in
guard let cost = entry.costUSD else { return nil }
return InlineUsageDashboardModel.Point(
id: entry.date,
label: Self.shortDayLabel(entry.date),
value: cost,
accessibilityValue: "\(entry.date): \(Self.costString(cost, currencyCode: snapshot.currencyCode))")
}
let latest = CostUsageTokenSnapshot.latestEntry(in: snapshot.daily)
let usesLatestPrimary = provider == .bedrock || provider == .mistral
let primaryCostUSD = usesLatestPrimary ? latest?.costUSD : snapshot.sessionCostUSD
var details: [String] = []
if comparisonPeriodsEnabled {
details.append(contentsOf: snapshot.comparisonSummaries().map {
Self.costWindowLine(summary: $0, currencyCode: snapshot.currencyCode)
})
}
if let topModel = Self.topCostModel(from: snapshot.daily) {
details.append("\(L("Top model")): \(Self.shortModelName(topModel))")
}
if let requestCount = snapshot.last30DaysRequests {
details.append("\(requestHistoryTitle): \(UsageFormatter.tokenCountString(requestCount)) \(L("requests"))")
}
if let hint = Self.tokenUsageHint(provider: provider) {
details.append(hint)
} else {
details.append(L("cost_estimate_hint"))
}
let providerName = ProviderDefaults.metadata[provider]?.displayName ?? provider.rawValue
var model = InlineUsageDashboardModel(
accessibilityLabel: "\(providerName) \(periodLabel) cost trend",
valueStyle: Self.costValueStyle(currencyCode: snapshot.currencyCode),
kpis: [
.init(
title: usesLatestPrimary ? L("Latest") : L("Today"),
value: primaryCostUSD.map { Self.costString($0, currencyCode: snapshot.currencyCode) } ?? "",
emphasis: true),
.init(
title: historyTitle,
value: snapshot.last30DaysCostUSD
.map { Self.costString($0, currencyCode: snapshot.currencyCode) } ?? "",
emphasis: false),
.init(
title: tokenHistoryTitle,
value: snapshot.last30DaysTokens.map(UsageFormatter.tokenCountString) ?? "",
emphasis: false),
] + Self.costHistoryTrailingKPIs(snapshot: snapshot, latest: latest),
points: points,
detailLines: details)
model.currencyCode = snapshot.currencyCode
return model
}
private static func costHistoryTrailingKPIs(
snapshot: CostUsageTokenSnapshot,
latest: CostUsageDailyReport.Entry?)
-> [InlineUsageDashboardModel.KPI]
{
if let requests = snapshot.last30DaysRequests {
return [
.init(
title: L("Requests"),
value: UsageFormatter.tokenCountString(requests),
emphasis: false),
]
}
return [
.init(
title: L("Latest tokens"),
value: latest?.totalTokens.map(UsageFormatter.tokenCountString) ?? "",
emphasis: false),
]
}
fileprivate static func claudeAdminAPIInlineDashboard(_ usage: ClaudeAdminAPIUsageSnapshot)
-> InlineUsageDashboardModel
{
let today = usage.currentDay
let last7 = usage.last7Days
let last30 = usage.last30Days
let points = usage.daily.suffix(30).map {
InlineUsageDashboardModel.Point(
id: $0.day,
label: Self.shortDayLabel($0.day),
value: $0.costUSD,
accessibilityValue: "\($0.day): \(UsageFormatter.usdString($0.costUSD))")
}
var details = [
"30d: \(UsageFormatter.tokenCountString(last30.totalTokens)) \(L("tokens"))",
"\(L("Cache read")): \(UsageFormatter.tokenCountString(last30.cacheReadInputTokens)) \(L("tokens"))",
]
if let topModel = usage.topModels.first {
details.append("\(L("Top model")): \(Self.shortModelName(topModel.name))")
}
var model = InlineUsageDashboardModel(
accessibilityLabel: L("Claude Admin API 30 day spend trend"),
valueStyle: .currencyUSD,
kpis: [
.init(title: L("Today"), value: UsageFormatter.usdString(today.costUSD), emphasis: true),
.init(title: L("7d spend"), value: UsageFormatter.usdString(last7.costUSD), emphasis: false),
.init(
title: L("30d spend"),
value: UsageFormatter.usdString(last30.costUSD),
emphasis: false),
.init(
title: L("Today tokens"),
value: UsageFormatter.tokenCountString(today.totalTokens),
emphasis: false),
],
points: points,
detailLines: details)
model.currencyCode = "USD"
return model
}
private static func crossModelInlineDashboard(_ usage: CrossModelUsageSnapshot) -> InlineUsageDashboardModel? {
let periodValues: [(String, String, Double?)] = [
("day", L("Today"), usage.daily?.cost),
("week", L("Week"), usage.weekly?.cost),
("month", L("Month"), usage.monthly?.cost),
]
let points = periodValues.compactMap { id, label, value -> InlineUsageDashboardModel.Point? in
guard let value else { return nil }
return InlineUsageDashboardModel.Point(
id: id,
label: label,
value: value,
accessibilityValue: "\(label): \(usage.currencyString(value))")
}
var model = InlineUsageDashboardModel(
accessibilityLabel: L("CrossModel API spend trend"),
valueStyle: Self.costValueStyle(currencyCode: usage.currency),
kpis: [
.init(title: L("Balance"), value: usage.balanceDisplay, emphasis: true),
.init(
title: L("Today"),
value: usage.daily.map { usage.currencyString($0.cost) } ?? "",
emphasis: false),
.init(
title: L("Week"),
value: usage.weekly.map { usage.currencyString($0.cost) } ?? "",
emphasis: false),
.init(
title: L("Month"),
value: usage.monthly.map { usage.currencyString($0.cost) } ?? "",
emphasis: false),
],
points: points,
detailLines: [])
model.currencyCode = usage.currency
return model
}
private static func openRouterInlineDashboard(_ usage: OpenRouterUsageSnapshot) -> InlineUsageDashboardModel? {
let periodValues: [(String, String, Double?)] = [
("day", L("Today"), usage.keyUsageDaily),
("week", L("Week"), usage.keyUsageWeekly),
("month", L("Month"), usage.keyUsageMonthly),
]
let points = periodValues.compactMap { id, label, value -> InlineUsageDashboardModel.Point? in
guard let value else { return nil }
let formattedValue = Self.openRouterCurrencyString(value)
return InlineUsageDashboardModel.Point(
id: id,
label: label,
value: value,
accessibilityValue: String(format: L("%@: %@"), label, formattedValue))
}
guard !points.isEmpty else { return nil }
var details: [String] = []
if let rate = usage.rateLimit {
details.append(String(format: L("Rate limit: %d / %@"), rate.requests, rate.interval))
}
switch usage.keyQuotaStatus {
case .available:
if let remaining = usage.keyRemaining {
details.append(String(
format: L("%@: %@"),
L("Key remaining"),
Self.openRouterCurrencyString(remaining)))
}
case .noLimitConfigured:
details.append(L("No limit set for the API key"))
case .unavailable:
details.append(L("API key limit unavailable right now"))
}
var model = InlineUsageDashboardModel(
accessibilityLabel: L("OpenRouter API key spend trend"),
valueStyle: .currencyUSD,
kpis: [
.init(title: L("Balance"), value: Self.openRouterCurrencyString(usage.balance), emphasis: true),
.init(
title: L("Today"),
value: usage.keyUsageDaily.map(Self.openRouterCurrencyString) ?? "",
emphasis: false),
.init(
title: L("Week"),
value: usage.keyUsageWeekly.map(Self.openRouterCurrencyString) ?? "",
emphasis: false),
.init(
title: L("Month"),
value: usage.keyUsageMonthly.map(Self.openRouterCurrencyString) ?? "",
emphasis: false),
],
points: points,
detailLines: details)
model.currencyCode = "USD"
return model
}
private static func zaiInlineDashboard(modelUsage: ZaiModelUsageData, now: Date) -> InlineUsageDashboardModel? {
let bars = ZaiHourlyBars.from(modelData: modelUsage, range: .last24h, now: now)
guard !bars.isEmpty else { return nil }
let total = bars.reduce(0) { $0 + $1.totalTokens }
let latest = bars.last
let peak = bars.max { $0.totalTokens < $1.totalTokens }
let points = bars.enumerated().map { index, bar in
InlineUsageDashboardModel.Point(
id: "\(index)-\(bar.label)",
label: bar.label,
value: Double(bar.totalTokens),
accessibilityValue: "\(bar.label): \(UsageFormatter.tokenCountString(bar.totalTokens)) \(L("tokens"))")
}
let topModel = Self.topZaiModel(from: bars)
return InlineUsageDashboardModel(
accessibilityLabel: L("z.ai hourly token trend"),
valueStyle: .tokens,
kpis: [
.init(title: L("24h tokens"), value: UsageFormatter.tokenCountString(total), emphasis: true),
.init(
title: L("Latest hour"),
value: latest.map { UsageFormatter.tokenCountString($0.totalTokens) } ?? "",
emphasis: false),
.init(
title: L("Peak hour"),
value: peak.map { UsageFormatter.tokenCountString($0.totalTokens) } ?? "",
emphasis: false),
.init(title: L("Models"), value: "\(modelUsage.modelNames.count)", emphasis: false),
],
points: points,
detailLines: topModel.map { ["\(L("Top model")): \(Self.shortModelName($0))"] } ?? [])
}
private static func minimaxInlineDashboard(_ billing: MiniMaxBillingSummary) -> InlineUsageDashboardModel {
let points = billing.daily.suffix(30).map {
InlineUsageDashboardModel.Point(
id: $0.day,
label: Self.shortDayLabel($0.day),
value: Double($0.tokens),
accessibilityValue: "\($0.day): \(UsageFormatter.tokenCountString($0.tokens)) \(L("tokens"))")
}
var details = [L("30d billing history from MiniMax web session")]
if let topModel = billing.topModels.first {
details.append("\(L("Top model")): \(Self.shortModelName(topModel.name))")
}
if let topMethod = billing.topMethods.first {
details.append("\(L("Top method")): \(Self.shortModelName(topMethod.name))")
}
if let cash = billing.last30DaysCash {
details.append("\(L("30d cash")): \(Self.minimaxCashString(cash))")
}
return InlineUsageDashboardModel(
accessibilityLabel: L("MiniMax 30 day token usage trend"),
valueStyle: .tokens,
kpis: [
.init(
title: L("Today"),
value: UsageFormatter.tokenCountString(billing.todayTokens),
emphasis: true),
.init(
title: L("30d tokens"),
value: UsageFormatter.tokenCountString(billing.last30DaysTokens),
emphasis: false),
.init(
title: L("Today cash"),
value: billing.todayCash.map(Self.minimaxCashString) ?? "",
emphasis: false),
.init(
title: L("Models"),
value: "\(billing.topModels.count)",
emphasis: false),
],
points: points,
detailLines: details)
}
private static func deepseekInlineDashboard(_ usage: DeepSeekUsageSummary) -> InlineUsageDashboardModel {
let symbol = usage.currency == "CNY" ? "¥" : "$"
let points = usage.daily.suffix(30).map {
InlineUsageDashboardModel.Point(
id: $0.date,
label: Self.shortDayLabel($0.date),
value: Double($0.totalTokens),
accessibilityValue: "\($0.date): \(UsageFormatter.tokenCountString($0.totalTokens)) \(L("tokens"))")
}
var details: [String] = []
if let topModel = usage.topModel {
details.append("\(L("Top model")): \(Self.shortModelName(topModel))")
}
if let cacheHit = usage.categoryBreakdown.first(where: { $0.category == .promptCacheHitToken }) {
details.append("\(L("cache-hit input")): \(UsageFormatter.tokenCountString(cacheHit.tokens))")
}
if let cacheMiss = usage.categoryBreakdown.first(where: { $0.category == .promptCacheMissToken }) {
details.append("\(L("cache-miss input")): \(UsageFormatter.tokenCountString(cacheMiss.tokens))")
}
if let output = usage.categoryBreakdown.first(where: { $0.category == .responseToken }) {
details.append("\(L("output")): \(UsageFormatter.tokenCountString(output.tokens))")
}
details.append("\(L("requests")): \(usage.currentMonthRequestCount)")
let todayCostStr = usage.todayCost.map { "\(symbol)\(String(format: "%.4f", max(0, $0)))" } ?? ""
let monthCostStr = usage.currentMonthCost.map { "\(symbol)\(String(format: "%.4f", max(0, $0)))" } ?? ""
let monthTokensStr = UsageFormatter.tokenCountString(usage.currentMonthTokens)
return InlineUsageDashboardModel(
accessibilityLabel: L("DeepSeek 30 day token usage trend"),
valueStyle: .tokens,
kpis: [
.init(
title: L("Today"),
value: "\(todayCostStr) · \(UsageFormatter.tokenCountString(usage.todayTokens))",
emphasis: true),
.init(
title: L("This month"),
value: "\(monthCostStr) · \(monthTokensStr)",
emphasis: false),
.init(
title: L("Models"),
value: usage.topModel.map { Self.shortModelName($0) } ?? "",
emphasis: false),
.init(
title: L("Requests"),
value: "\(usage.currentMonthRequestCount)",
emphasis: false),
],
points: points,
detailLines: details)
}
private static func topMistralModel(from entries: [MistralDailyUsageBucket]) -> String? {
var tokens: [String: Int] = [:]
for entry in entries {
for model in entry.models {
tokens[model.name, default: 0] += model.totalTokens
}
}
return tokens.max {
if $0.value == $1.value { return $0.key > $1.key }
return $0.value < $1.value
}?.key
}
private static func topZaiModel(from bars: [ZaiHourlyBar]) -> String? {
var tokens: [String: Int] = [:]
for bar in bars {
for segment in bar.segments {
tokens[segment.model, default: 0] += segment.tokens
}
}
return tokens.max {
if $0.value == $1.value { return $0.key > $1.key }
return $0.value < $1.value
}?.key
}
private static func openRouterCurrencyString(_ value: Double) -> String {
String(format: "$%.2f", value)
}
private static func minimaxCashString(_ value: Double) -> String {
String(format: "%.2f", max(0, value))
}
private static func costString(_ value: Double, currencyCode: String) -> String {
UsageFormatter.currencyString(value, currencyCode: currencyCode)
}
private static func costValueStyle(currencyCode: String) -> InlineUsageDashboardModel.ValueStyle {
if currencyCode == "USD" { return .currencyUSD }
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencyCode = currencyCode
formatter.locale = Locale(identifier: "en_US")
let symbol = formatter.currencySymbol ?? currencyCode
return .currency(symbol: symbol)
}
private static func shortDayLabel(_ day: String) -> String {
let pieces = day.split(separator: "-")
guard pieces.count == 3, let rawDay = Int(pieces[2]) else { return day }
return "\(rawDay)"
}
private static func shortModelName(_ name: String) -> String {
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard trimmed.count > 26 else { return trimmed }
return String(trimmed.prefix(25)) + ""
}
private static func topCostModel(from entries: [CostUsageDailyReport.Entry]) -> String? {
var scores: [String: (cost: Double, tokens: Int)] = [:]
for entry in entries {
for model in entry.modelBreakdowns ?? [] {
var score = scores[model.modelName] ?? (0, 0)
score.cost += model.costUSD ?? 0
score.tokens += model.totalTokens ?? 0
scores[model.modelName] = score
}
}
return scores.max {
if $0.value.cost == $1.value.cost { return $0.value.tokens < $1.value.tokens }
return $0.value.cost < $1.value.cost
}?.key
}
}
struct InlineUsageDashboardContent: View {
private let model: InlineUsageDashboardModel
@Environment(\.menuItemHighlighted) private var isHighlighted
init(model: InlineUsageDashboardModel) {
self.model = model
}
var body: some View {
VStack(alignment: .leading, spacing: 10) {
self.kpis
if !self.model.points.isEmpty {
MiniUsageBars(model: self.model)
.frame(height: 58)
.accessibilityLabel(self.model.accessibilityLabel)
}
self.detailLines
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private var kpis: some View {
LazyVGrid(
columns: [
GridItem(.flexible(minimum: 118), alignment: .leading),
GridItem(.flexible(minimum: 100), alignment: .leading),
],
alignment: .leading,
spacing: 6)
{
ForEach(Array(self.model.kpis.enumerated()), id: \.offset) { _, kpi in
KPIBlock(title: kpi.title, value: kpi.value, emphasis: kpi.emphasis)
}
}
}
private var detailLines: some View {
VStack(alignment: .leading, spacing: 3) {
ForEach(Array(self.model.detailLines.enumerated()), id: \.offset) { _, line in
Text(line)
.font(.caption)
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
.lineLimit(1)
}
}
}
private struct KPIBlock: View {
let title: String
let value: String
let emphasis: Bool
@Environment(\.menuItemHighlighted) private var isHighlighted
var body: some View {
VStack(alignment: .leading, spacing: 1) {
Text(self.title)
.font(.caption2)
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
.lineLimit(1)
Text(self.value)
.font(self.emphasis ? .headline : .subheadline)
.fontWeight(.semibold)
.foregroundStyle(MenuHighlightStyle.primary(self.isHighlighted))
.lineLimit(1)
.minimumScaleFactor(0.72)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
private struct MiniUsageBars: View {
let model: InlineUsageDashboardModel
@Environment(\.menuItemHighlighted) private var isHighlighted
var body: some View {
let scale = UsageChartScale(values: self.model.points.map(\.value))
VStack(alignment: .trailing, spacing: 2) {
if let currencyCode = self.model.currencyCode, scale.maximum > 0 {
Text(UsageFormatter.compactCurrencyString(scale.maximum, currencyCode: currencyCode))
.font(.caption2)
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
.monospacedDigit()
.lineLimit(1)
.allowsTightening(true)
}
GeometryReader { geometry in
HStack(alignment: .bottom, spacing: 2) {
ForEach(self.model.points) { point in
RoundedRectangle(cornerRadius: 1.5, style: .continuous)
.fill(self.fill(for: point, scale: scale))
.frame(maxWidth: .infinity)
.frame(height: self.height(for: point, scale: scale, available: geometry.size.height))
.accessibilityLabel(point.accessibilityValue)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.overlay(alignment: .bottomLeading) {
Rectangle()
.fill(MenuHighlightStyle.secondary(self.isHighlighted).opacity(0.22))
.frame(height: 1)
}
}
}
}
private func height(
for point: InlineUsageDashboardModel.Point,
scale: UsageChartScale,
available: CGFloat) -> CGFloat
{
let ratio = scale.fraction(for: point.value)
guard ratio > 0 else { return 1 }
return max(3, CGFloat(ratio) * available)
}
private func fill(for point: InlineUsageDashboardModel.Point, scale: UsageChartScale) -> Color {
let ratio = max(0.18, scale.fraction(for: point.value))
if self.isHighlighted {
return Color.white.opacity(0.55 + ratio * 0.35)
}
return self.baseColor.opacity(0.42 + ratio * 0.58)
}
private var baseColor: Color {
if let barColor = self.model.barColor {
return barColor
}
switch self.model.valueStyle {
case .currencyUSD, .currency:
return Color(red: 0.81, green: 0.56, blue: 0.24)
case .tokens:
return Color(red: 0.48, green: 0.41, blue: 0.86)
case .points:
return Color(red: 0.16, green: 0.62, blue: 0.36)
}
}
}
}
+9
View File
@@ -0,0 +1,9 @@
import Foundation
enum InstallOrigin {
static func isHomebrewCask(appBundleURL: URL) -> Bool {
let resolved = appBundleURL.resolvingSymlinksInPath()
let path = resolved.path
return path.contains("/Caskroom/") || path.contains("/Homebrew/Caskroom/")
}
}
@@ -0,0 +1,6 @@
import KeyboardShortcuts
@MainActor
extension KeyboardShortcuts.Name {
static let openMenu = Self("openMenu")
}
+154
View File
@@ -0,0 +1,154 @@
import CodexBarCore
import Foundation
import Security
/// Migrates keychain items to use kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
/// to prevent permission prompts on every rebuild during development.
enum KeychainMigration {
private static let log = CodexBarLog.logger(LogCategories.keychainMigration)
private static let migrationKey = "KeychainMigrationV1Completed"
struct MigrationItem: Hashable {
let service: String
let account: String?
var label: String {
let accountLabel = self.account ?? "<any>"
return "\(self.service):\(accountLabel)"
}
}
static let itemsToMigrate: [MigrationItem] = [
MigrationItem(service: "com.steipete.CodexBar", account: "codex-cookie"),
MigrationItem(service: "com.steipete.CodexBar", account: "claude-cookie"),
MigrationItem(service: "com.steipete.CodexBar", account: "cursor-cookie"),
MigrationItem(service: "com.steipete.CodexBar", account: "factory-cookie"),
MigrationItem(service: "com.steipete.CodexBar", account: "minimax-cookie"),
MigrationItem(service: "com.steipete.CodexBar", account: "minimax-api-token"),
MigrationItem(service: "com.steipete.CodexBar", account: "augment-cookie"),
MigrationItem(service: "com.steipete.CodexBar", account: "copilot-api-token"),
MigrationItem(service: "com.steipete.CodexBar", account: "zai-api-token"),
MigrationItem(service: "com.steipete.CodexBar", account: "synthetic-api-key"),
]
/// Run migration once per installation
static func migrateIfNeeded() {
guard !KeychainAccessGate.isDisabled else {
self.log.info("Keychain access disabled; skipping migration")
return
}
if !UserDefaults.standard.bool(forKey: self.migrationKey) {
self.log.info("Starting keychain migration to reduce permission prompts")
var migratedCount = 0
var errorCount = 0
for item in self.itemsToMigrate {
do {
if try self.migrateItem(item) {
migratedCount += 1
}
} catch {
errorCount += 1
self.log.error("Failed to migrate \(item.label): \(String(describing: error))")
}
}
self.log.info("Keychain migration complete: \(migratedCount) migrated, \(errorCount) errors")
UserDefaults.standard.set(true, forKey: self.migrationKey)
if migratedCount > 0 {
self.log.info("✅ Future rebuilds will not prompt for keychain access")
}
} else {
self.log.debug("Keychain migration already completed, skipping")
}
}
/// Migrate a single keychain item to the new accessibility level
/// Returns true if item was migrated, false if item didn't exist
private static func migrateItem(_ item: MigrationItem) throws -> Bool {
// First, try to read the existing item
var result: CFTypeRef?
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: item.service,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true,
kSecReturnAttributes as String: true,
]
if let account = item.account {
query[kSecAttrAccount as String] = account
}
let status = KeychainSecurity.copyMatching(query as CFDictionary, &result)
if status == errSecItemNotFound {
// Item doesn't exist, nothing to migrate
return false
}
guard status == errSecSuccess else {
throw KeychainMigrationError.readFailed(status)
}
guard let rawItem = result as? [String: Any],
let data = rawItem[kSecValueData as String] as? Data,
let accessible = rawItem[kSecAttrAccessible as String] as? String
else {
throw KeychainMigrationError.invalidItemFormat
}
// Check if already using the correct accessibility
if accessible == (kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String) {
self.log.debug("\(item.label) already using correct accessibility")
return false
}
// Delete the old item
var deleteQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: item.service,
]
if let account = item.account {
deleteQuery[kSecAttrAccount as String] = account
}
let deleteStatus = KeychainSecurity.delete(deleteQuery as CFDictionary)
guard deleteStatus == errSecSuccess else {
throw KeychainMigrationError.deleteFailed(deleteStatus)
}
// Add it back with the new accessibility
var addQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: item.service,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
]
if let account = item.account {
addQuery[kSecAttrAccount as String] = account
}
let addStatus = KeychainSecurity.add(addQuery as CFDictionary, nil)
guard addStatus == errSecSuccess else {
throw KeychainMigrationError.addFailed(addStatus)
}
self.log.info("Migrated \(item.label) to new accessibility level")
return true
}
/// Reset migration flag (for testing)
static func resetMigrationFlag() {
UserDefaults.standard.removeObject(forKey: self.migrationKey)
}
}
enum KeychainMigrationError: Error {
case readFailed(OSStatus)
case deleteFailed(OSStatus)
case addFailed(OSStatus)
case invalidItemFormat
}
@@ -0,0 +1,222 @@
import AppKit
import CodexBarCore
import SweetCookieKit
private enum KeychainPromptMessage {
static let browserCookie =
"CodexBar will ask macOS Keychain for “%@” so it can decrypt browser cookies " +
"and authenticate your account. Click OK to continue."
static let claudeOAuth =
"CodexBar will ask macOS Keychain for the Claude Code OAuth token " +
"so it can fetch your Claude usage. Click OK to continue."
static let codexCookie =
"CodexBar will ask macOS Keychain for your OpenAI cookie header " +
"so it can fetch Codex dashboard extras. Click OK to continue."
static let claudeCookie =
"CodexBar will ask macOS Keychain for your Claude cookie header " +
"so it can fetch Claude web usage. Click OK to continue."
static let cursorCookie =
"CodexBar will ask macOS Keychain for your Cursor cookie header " +
"so it can fetch usage. Click OK to continue."
static let openCodeCookie =
"CodexBar will ask macOS Keychain for your OpenCode cookie header " +
"so it can fetch usage. Click OK to continue."
static let factoryCookie =
"CodexBar will ask macOS Keychain for your Factory cookie header " +
"so it can fetch usage. Click OK to continue."
static let zaiToken =
"CodexBar will ask macOS Keychain for your z.ai API token " +
"so it can fetch usage. Click OK to continue."
static let syntheticToken =
"CodexBar will ask macOS Keychain for your Synthetic API key " +
"so it can fetch usage. Click OK to continue."
static let copilotToken =
"CodexBar will ask macOS Keychain for your GitHub Copilot token " +
"so it can fetch usage. Click OK to continue."
static let kimiToken =
"CodexBar will ask macOS Keychain for your Kimi auth token " +
"so it can fetch usage. Click OK to continue."
static let kimiK2Token =
"CodexBar will ask macOS Keychain for your Kimi K2 API key " +
"so it can fetch usage. Click OK to continue."
static let minimaxCookie =
"CodexBar will ask macOS Keychain for your MiniMax cookie header " +
"so it can fetch usage. Click OK to continue."
static let minimaxToken =
"CodexBar will ask macOS Keychain for your MiniMax API token " +
"so it can fetch usage. Click OK to continue."
static let augmentCookie =
"CodexBar will ask macOS Keychain for your Augment cookie header " +
"so it can fetch usage. Click OK to continue."
static let ampCookie =
"CodexBar will ask macOS Keychain for your Amp cookie header " +
"so it can fetch usage. Click OK to continue."
}
struct KeychainPromptAlertModel: Equatable {
let title: String
let message: String
let primaryButtonTitle: String
let learnMoreButtonTitle: String
let documentationURL: String
}
@MainActor
private final class KeychainPromptLearnMoreTarget: NSObject {
private let documentationURL: String
init(documentationURL: String) {
self.documentationURL = documentationURL
}
@objc func openDocumentation() {
guard let url = URL(string: self.documentationURL) else { return }
NSWorkspace.shared.open(url)
}
}
enum KeychainPromptCoordinator {
private static let promptLock = NSLock()
private static let log = CodexBarLog.logger(LogCategories.keychainPrompt)
private static let documentationURL =
"https://github.com/steipete/CodexBar/blob/main/docs/keychain-prompts.md"
static func install() {
KeychainPromptHandler.handler = { context in
self.presentKeychainPrompt(context)
}
BrowserCookieKeychainPromptHandler.handler = { context in
self.presentBrowserCookiePrompt(context)
}
self.disableKeychainForUnbundledExecutableIfNeeded()
}
private static let unbundledExecutableCheckLock = NSLock()
private nonisolated(unsafe) static var didCheckUnbundledExecutable = false
static func disableKeychainForUnbundledExecutableIfNeeded() {
self.unbundledExecutableCheckLock.lock()
guard !self.didCheckUnbundledExecutable else {
self.unbundledExecutableCheckLock.unlock()
return
}
self.didCheckUnbundledExecutable = true
self.unbundledExecutableCheckLock.unlock()
let executablePath = Bundle.main.executableURL?.path ?? ""
guard Self.isUnbundledCodexBarExecutable(executablePath) else { return }
KeychainAccessGate.forceDisabledForProcess(reason: "unbundled-executable")
Self.log.warning(
"Unbundled CodexBar executable detected; disabling keychain access to avoid repeated prompts",
metadata: ["doc": "docs/DEVELOPMENT_SETUP.md"])
}
static func isUnbundledCodexBarExecutable(_ executablePath: String) -> Bool {
guard executablePath.hasPrefix("/") else { return false }
let executableURL = URL(fileURLWithPath: executablePath).standardizedFileURL
return executableURL.lastPathComponent == "CodexBar"
&& !executableURL.pathComponents.contains(where: { $0.hasSuffix(".app") })
}
private static func presentKeychainPrompt(_ context: KeychainPromptContext) {
let model = self.alertModel(for: context)
self.log.info("Keychain prompt requested", metadata: ["kind": "\(context.kind)"])
self.presentAlert(model)
}
private static func presentBrowserCookiePrompt(_ context: BrowserCookieKeychainPromptContext) {
let model = self.browserCookieAlertModel(label: context.label)
self.log.info("Browser cookie keychain prompt requested", metadata: ["label": context.label])
self.presentAlert(model)
}
static func alertModel(for context: KeychainPromptContext) -> KeychainPromptAlertModel {
let purpose = switch context.kind {
case .claudeOAuth:
L(KeychainPromptMessage.claudeOAuth)
case .codexCookie:
L(KeychainPromptMessage.codexCookie)
case .claudeCookie:
L(KeychainPromptMessage.claudeCookie)
case .cursorCookie:
L(KeychainPromptMessage.cursorCookie)
case .opencodeCookie:
L(KeychainPromptMessage.openCodeCookie)
case .factoryCookie:
L(KeychainPromptMessage.factoryCookie)
case .zaiToken:
L(KeychainPromptMessage.zaiToken)
case .syntheticToken:
L(KeychainPromptMessage.syntheticToken)
case .copilotToken:
L(KeychainPromptMessage.copilotToken)
case .kimiToken:
L(KeychainPromptMessage.kimiToken)
case .kimiK2Token:
L(KeychainPromptMessage.kimiK2Token)
case .minimaxCookie:
L(KeychainPromptMessage.minimaxCookie)
case .minimaxToken:
L(KeychainPromptMessage.minimaxToken)
case .augmentCookie:
L(KeychainPromptMessage.augmentCookie)
case .ampCookie:
L(KeychainPromptMessage.ampCookie)
}
return self.alertModel(purpose: purpose)
}
static func browserCookieAlertModel(label: String) -> KeychainPromptAlertModel {
self.alertModel(purpose: L(KeychainPromptMessage.browserCookie, label))
}
private static func alertModel(purpose: String) -> KeychainPromptAlertModel {
KeychainPromptAlertModel(
title: L("Keychain Access Required"),
message: "\(purpose)\n\n\(L("keychain_prompt_privacy_note"))",
primaryButtonTitle: L("OK"),
learnMoreButtonTitle: L("keychain_prompt_learn_more"),
documentationURL: self.documentationURL)
}
private static func presentAlert(_ model: KeychainPromptAlertModel) {
self.promptLock.lock()
defer { self.promptLock.unlock() }
if Thread.isMainThread {
MainActor.assumeIsolated {
self.showAlert(model)
}
return
}
DispatchQueue.main.sync {
MainActor.assumeIsolated {
self.showAlert(model)
}
}
}
@MainActor
private static func showAlert(_ model: KeychainPromptAlertModel) {
let alert = NSAlert()
alert.messageText = model.title
alert.informativeText = model.message
alert.addButton(withTitle: model.primaryButtonTitle)
let learnMoreTarget = KeychainPromptLearnMoreTarget(documentationURL: model.documentationURL)
let learnMoreButton = NSButton(
title: model.learnMoreButtonTitle,
target: learnMoreTarget,
action: #selector(KeychainPromptLearnMoreTarget.openDocumentation))
learnMoreButton.isBordered = false
learnMoreButton.contentTintColor = .linkColor
learnMoreButton.font = .systemFont(ofSize: NSFont.smallSystemFontSize)
learnMoreButton.sizeToFit()
alert.accessoryView = learnMoreButton
withExtendedLifetime(learnMoreTarget) {
_ = alert.runModal()
}
}
}
+128
View File
@@ -0,0 +1,128 @@
import CodexBarCore
import Foundation
import Security
protocol KimiK2TokenStoring: Sendable {
func loadToken() throws -> String?
func storeToken(_ token: String?) throws
}
enum KimiK2TokenStoreError: LocalizedError {
case keychainStatus(OSStatus)
case invalidData
var errorDescription: String? {
switch self {
case let .keychainStatus(status):
"Keychain error: \(status)"
case .invalidData:
"Keychain returned invalid data."
}
}
}
struct KeychainKimiK2TokenStore: KimiK2TokenStoring {
private static let log = CodexBarLog.logger(LogCategories.kimiK2TokenStore)
private let service = "com.steipete.CodexBar"
private let account = "kimi-k2-api-token"
func loadToken() throws -> String? {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping token load")
return nil
}
var result: CFTypeRef?
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true,
]
if case .interactionRequired = KeychainAccessPreflight
.checkGenericPassword(service: self.service, account: self.account)
{
KeychainPromptHandler.handler?(KeychainPromptContext(
kind: .kimiK2Token,
service: self.service,
account: self.account))
}
let status = KeychainSecurity.copyMatching(query as CFDictionary, &result)
if status == errSecItemNotFound {
return nil
}
guard status == errSecSuccess else {
Self.log.error("Keychain read failed: \(status)")
throw KimiK2TokenStoreError.keychainStatus(status)
}
guard let data = result as? Data else {
throw KimiK2TokenStoreError.invalidData
}
let token = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
if let token, !token.isEmpty {
return token
}
return nil
}
func storeToken(_ token: String?) throws {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping token store")
return
}
let cleaned = token?.trimmingCharacters(in: .whitespacesAndNewlines)
if cleaned == nil || cleaned?.isEmpty == true {
try self.deleteTokenIfPresent()
return
}
let data = cleaned!.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let attributes: [String: Any] = [
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
]
let updateStatus = KeychainSecurity.update(query as CFDictionary, attributes as CFDictionary)
if updateStatus == errSecSuccess {
return
}
if updateStatus != errSecItemNotFound {
Self.log.error("Keychain update failed: \(updateStatus)")
throw KimiK2TokenStoreError.keychainStatus(updateStatus)
}
var addQuery = query
for (key, value) in attributes {
addQuery[key] = value
}
let addStatus = KeychainSecurity.add(addQuery as CFDictionary, nil)
guard addStatus == errSecSuccess else {
Self.log.error("Keychain add failed: \(addStatus)")
throw KimiK2TokenStoreError.keychainStatus(addStatus)
}
}
private func deleteTokenIfPresent() throws {
guard !KeychainAccessGate.isDisabled else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let status = KeychainSecurity.delete(query as CFDictionary)
if status == errSecSuccess || status == errSecItemNotFound {
return
}
Self.log.error("Keychain delete failed: \(status)")
throw KimiK2TokenStoreError.keychainStatus(status)
}
}
+128
View File
@@ -0,0 +1,128 @@
import CodexBarCore
import Foundation
import Security
protocol KimiTokenStoring: Sendable {
func loadToken() throws -> String?
func storeToken(_ token: String?) throws
}
enum KimiTokenStoreError: LocalizedError {
case keychainStatus(OSStatus)
case invalidData
var errorDescription: String? {
switch self {
case let .keychainStatus(status):
"Keychain error: \(status)"
case .invalidData:
"Keychain returned invalid data."
}
}
}
struct KeychainKimiTokenStore: KimiTokenStoring {
private static let log = CodexBarLog.logger(LogCategories.kimiTokenStore)
private let service = "com.steipete.CodexBar"
private let account = "kimi-auth-token"
func loadToken() throws -> String? {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping token load")
return nil
}
var result: CFTypeRef?
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true,
]
if case .interactionRequired = KeychainAccessPreflight
.checkGenericPassword(service: self.service, account: self.account)
{
KeychainPromptHandler.handler?(KeychainPromptContext(
kind: .kimiToken,
service: self.service,
account: self.account))
}
let status = KeychainSecurity.copyMatching(query as CFDictionary, &result)
if status == errSecItemNotFound {
return nil
}
guard status == errSecSuccess else {
Self.log.error("Keychain read failed: \(status)")
throw KimiTokenStoreError.keychainStatus(status)
}
guard let data = result as? Data else {
throw KimiTokenStoreError.invalidData
}
let token = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
if let token, !token.isEmpty {
return token
}
return nil
}
func storeToken(_ token: String?) throws {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping token store")
return
}
let cleaned = token?.trimmingCharacters(in: .whitespacesAndNewlines)
if cleaned == nil || cleaned?.isEmpty == true {
try self.deleteTokenIfPresent()
return
}
let data = cleaned!.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let attributes: [String: Any] = [
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
]
let updateStatus = KeychainSecurity.update(query as CFDictionary, attributes as CFDictionary)
if updateStatus == errSecSuccess {
return
}
if updateStatus != errSecItemNotFound {
Self.log.error("Keychain update failed: \(updateStatus)")
throw KimiTokenStoreError.keychainStatus(updateStatus)
}
var addQuery = query
for (key, value) in attributes {
addQuery[key] = value
}
let addStatus = KeychainSecurity.add(addQuery as CFDictionary, nil)
guard addStatus == errSecSuccess else {
Self.log.error("Keychain add failed: \(addStatus)")
throw KimiTokenStoreError.keychainStatus(addStatus)
}
}
private func deleteTokenIfPresent() throws {
guard !KeychainAccessGate.isDisabled else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let status = KeychainSecurity.delete(query as CFDictionary)
if status == errSecSuccess || status == errSecItemNotFound {
return
}
Self.log.error("Keychain delete failed: \(status)")
throw KimiTokenStoreError.keychainStatus(status)
}
}
@@ -0,0 +1,56 @@
import CodexBarCore
import ServiceManagement
enum LaunchAtLoginManager {
typealias StatusProvider = () -> SMAppService.Status
typealias RegistrationAction = () throws -> Void
private static let isRunningTests: Bool = {
let env = ProcessInfo.processInfo.environment
if env["XCTestConfigurationFilePath"] != nil { return true }
if env["TESTING_LIBRARY_VERSION"] != nil { return true }
if env["SWIFT_TESTING"] != nil { return true }
return NSClassFromString("XCTestCase") != nil
}()
static func setEnabled(_ enabled: Bool) {
if self.isRunningTests { return }
let service = SMAppService.mainApp
self.setEnabled(
enabled,
status: { service.status },
register: { try service.register() },
unregister: { try service.unregister() })
}
static func setEnabled(
_ enabled: Bool,
status: StatusProvider,
register: RegistrationAction,
unregister: RegistrationAction)
{
do {
if enabled {
switch status() {
case .enabled, .requiresApproval:
return
case .notRegistered, .notFound:
try register()
@unknown default:
try register()
}
} else {
switch status() {
case .enabled, .requiresApproval:
try unregister()
case .notRegistered, .notFound:
return
@unknown default:
try unregister()
}
}
} catch {
CodexBarLog.logger(LogCategories.launchAtLogin).error("Failed to update login item: \(error)")
}
}
}
+62
View File
@@ -0,0 +1,62 @@
import Foundation
enum LoadingPattern: String, CaseIterable, Identifiable {
case knightRider
case cylon
case outsideIn
case race
case pulse
case unbraid
var id: String {
self.rawValue
}
var displayName: String {
switch self {
case .knightRider: "Knight Rider"
case .cylon: "Cylon"
case .outsideIn: "Outside-In"
case .race: "Race"
case .pulse: "Pulse"
case .unbraid: "Unbraid (logo → bars)"
}
}
/// Secondary offset so the lower bar moves differently.
var secondaryOffset: Double {
switch self {
case .knightRider: .pi
case .cylon: .pi / 2
case .outsideIn: .pi
case .race: .pi / 3
case .pulse: .pi / 2
case .unbraid: .pi / 2
}
}
func value(phase: Double) -> Double {
let v: Double
switch self {
case .knightRider:
v = 0.5 + 0.5 * sin(phase) // ping-pong
case .cylon:
let t = phase.truncatingRemainder(dividingBy: .pi * 2) / (.pi * 2)
v = t // sawtooth 01
case .outsideIn:
v = abs(cos(phase)) // high at edges, dip center
case .race:
let t = (phase * 1.2).truncatingRemainder(dividingBy: .pi * 2) / (.pi * 2)
v = t
case .pulse:
v = 0.4 + 0.6 * (0.5 + 0.5 * sin(phase)) // 40100%
case .unbraid:
v = 0.5 + 0.5 * sin(phase) // smooth 01 for morph
}
return max(0, min(v * 100, 100))
}
}
extension Notification.Name {
static let codexbarDebugReplayAllAnimations = Notification.Name("codexbarDebugReplayAllAnimations")
}
+264
View File
@@ -0,0 +1,264 @@
import CodexBarCore
import Foundation
enum CodexBarLocalizationOverride {
@TaskLocal static var appLanguage: String?
}
enum AppLanguagePreferenceMigration {
private static let appleLanguagesKey = "AppleLanguages"
static func clearLegacyOverrideIfOwned(
storedAppLanguage: String,
defaults: UserDefaults = .standard)
{
let language = storedAppLanguage.trimmingCharacters(in: .whitespacesAndNewlines)
guard !language.isEmpty,
defaults.stringArray(forKey: self.appleLanguagesKey) == [language]
else { return }
defaults.removeObject(forKey: self.appleLanguagesKey)
}
}
private func appLanguageDefaults() -> UserDefaults {
if Bundle.main.bundleIdentifier != nil {
return .standard
}
if UserDefaults.standard.object(forKey: "appLanguage") != nil {
return .standard
}
// Fallback for running outside a .app bundle (swift run / debug builds)
return UserDefaults(suiteName: "CodexBar") ?? .standard
}
private let isRunningTestsProcessAtStartup: Bool = {
let env = ProcessInfo.processInfo.environment
if env["XCTestConfigurationFilePath"] != nil { return true }
if env["TESTING_LIBRARY_VERSION"] != nil { return true }
if env["SWIFT_TESTING"] != nil { return true }
return NSClassFromString("XCTestCase") != nil
}()
private func isRunningTestsProcess() -> Bool {
isRunningTestsProcessAtStartup
}
private func resolvedAppLanguage() -> String {
if let override = CodexBarLocalizationOverride.appLanguage {
return override
}
if isRunningTestsProcess() {
return "en"
}
return appLanguageDefaults().string(forKey: "appLanguage") ?? ""
}
func codexBarLocalizationSignature() -> String {
resolvedAppLanguage()
}
/// Resolving the `.lproj`/resource bundles repeats `Bundle(url:)`/`Bundle(path:)` filesystem lookups,
/// which are surprisingly hot: every `L()` and `codexBarLocalizationSignature()` call runs them, and
/// menu row bodies (`MetricRow`, `ProviderCostContent`, `UsageMenuCardView.Model`) re-evaluate them on
/// every closed-menu rebuild tick on the main thread (#1347). The resolved bundles never change unless
/// the language changes, so cache them. A single lock with compute-happening-outside-the-lock keeps the
/// disk work off the critical section and avoids re-entrant deadlock when the localized-bundle compute
/// closure calls back into the resource-bundle accessor.
private enum LocalizationBundleCache {
private static let lock = NSLock()
private nonisolated(unsafe) static var resourceBundle: Bundle?
private nonisolated(unsafe) static var localizedBundlesByLanguage: [String: Bundle] = [:]
static func defaultResourceBundle(_ compute: () -> Bundle) -> Bundle {
self.lock.lock()
if let resourceBundle {
self.lock.unlock()
return resourceBundle
}
self.lock.unlock()
let computed = compute()
self.lock.lock()
resourceBundle = computed
self.lock.unlock()
return computed
}
static func localizedBundle(forLanguage language: String, _ compute: () -> Bundle) -> Bundle {
self.lock.lock()
if let cachedLocalizedBundle = self.localizedBundlesByLanguage[language] {
self.lock.unlock()
return cachedLocalizedBundle
}
self.lock.unlock()
let computed = compute()
self.lock.lock()
self.localizedBundlesByLanguage[language] = computed
self.lock.unlock()
return computed
}
static func reset() {
self.lock.lock()
self.resourceBundle = nil
self.localizedBundlesByLanguage = [:]
self.lock.unlock()
}
}
func codexBarLocalizationResourceBundle(
mainBundle: Bundle = .main,
bundleName: String = "CodexBar_CodexBar") -> Bundle
{
// Only the default (process `.main`) resolution is cached: it is constant for the lifetime of the
// process. Custom arguments (tests) keep resolving directly so they stay isolated from the cache.
guard mainBundle === Bundle.main, bundleName == "CodexBar_CodexBar" else {
return resolveLocalizationResourceBundle(mainBundle: mainBundle, bundleName: bundleName)
}
return LocalizationBundleCache.defaultResourceBundle {
resolveLocalizationResourceBundle(mainBundle: mainBundle, bundleName: bundleName)
}
}
private func resolveLocalizationResourceBundle(mainBundle: Bundle, bundleName: String) -> Bundle {
guard mainBundle.bundleURL.pathExtension == "app" else {
return Bundle.module
}
if let url = mainBundle.url(forResource: bundleName, withExtension: "bundle"),
let bundle = Bundle(url: url)
{
return bundle
}
if let resourceURL = mainBundle.resourceURL?.absoluteURL,
let bundle = Bundle(url: resourceURL.appendingPathComponent("\(bundleName).bundle"))
{
return bundle
}
return mainBundle
}
private func localizedBundle() -> Bundle {
// Keyed on the resolved language so a language switch (settings change or test override) transparently
// re-resolves; otherwise the cached bundle is returned without touching the filesystem.
let language = resolvedAppLanguage()
return localizedBundle(forLanguage: language)
}
private func localizedBundle(forLanguage language: String) -> Bundle {
LocalizationBundleCache.localizedBundle(forLanguage: language) {
resolveLocalizedBundle(forLanguage: language)
}
}
private func resolveLocalizedBundle(forLanguage language: String) -> Bundle {
let resourceBundle = codexBarLocalizationResourceBundle()
if !language.isEmpty {
if let bundle = lprojBundle(named: language, in: resourceBundle) {
return bundle
}
} else {
// System mode: follow macOS language preferences
let localizations = resourceBundle.localizations.filter { $0 != "Base" }
let preferred = Bundle.preferredLocalizations(
from: localizations,
forPreferences: Locale.preferredLanguages).first
if let preferred,
let bundle = lprojBundle(named: preferred, in: resourceBundle)
{
return bundle
}
}
// Fallback to en.lproj
if let path = resourceBundle.path(forResource: "en", ofType: "lproj"),
let bundle = Bundle(path: path)
{
return bundle
}
return resourceBundle
}
private func lprojBundle(named language: String, in resourceBundle: Bundle) -> Bundle? {
let candidates = [language, language.lowercased()]
for candidate in candidates where !candidate.isEmpty {
if let path = resourceBundle.path(forResource: candidate, ofType: "lproj"),
let bundle = Bundle(path: path)
{
return bundle
}
}
return nil
}
func L(_ key: String) -> String {
let resourceBundle = codexBarLocalizationResourceBundle()
return codexBarLocalizedString(key, bundle: localizedBundle(), resourceBundle: resourceBundle)
}
func L(_ key: String, _ arguments: CVarArg...) -> String {
String(format: L(key), arguments: arguments)
}
func L(_ key: String, language: String) -> String {
let resourceBundle = codexBarLocalizationResourceBundle()
let bundle = localizedBundle(forLanguage: language)
return codexBarLocalizedString(key, bundle: bundle, resourceBundle: resourceBundle)
}
func codexBarLocalizedLocale() -> Locale {
let language = resolvedAppLanguage()
guard !language.isEmpty else { return .current }
switch language.lowercased() {
case "zh-hans":
return Locale(identifier: "zh-Hans")
case "zh-hant":
return Locale(identifier: "zh-Hant")
case "pt-br":
return Locale(identifier: "pt-BR")
default:
return Locale(identifier: language)
}
}
func codexBarLocalizedString(_ key: String, bundle: Bundle, resourceBundle: Bundle) -> String {
let value = bundle.localizedString(forKey: key, value: nil, table: nil)
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmed.isEmpty, value != key {
return value
}
guard bundle.bundleURL.lastPathComponent != "en.lproj",
let englishBundle = lprojBundle(named: "en", in: resourceBundle)
else {
return trimmed.isEmpty ? key : value
}
let fallback = englishBundle.localizedString(forKey: key, value: nil, table: nil)
return fallback.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? key : fallback
}
func resetCodexBarLocalizationCache() {
LocalizationBundleCache.reset()
}
#if DEBUG
func codexBarLocalizedBundleForTesting() -> Bundle {
localizedBundle()
}
func resetCodexBarLocalizationCacheForTesting() {
resetCodexBarLocalizationCache()
}
#endif
func configureUsageFormatterLocalizationProvider() {
UsageFormatter.setLocalizationProvider { key in
let resourceBundle = codexBarLocalizationResourceBundle()
return codexBarLocalizedString(key, bundle: localizedBundle(), resourceBundle: resourceBundle)
}
UsageFormatter.setLocaleProvider {
codexBarLocalizedLocale()
}
}
@@ -0,0 +1,311 @@
import CodexBarCore
import Foundation
/// Tracks what the main thread is currently doing so hang reports can name the
/// operation even when the stall happens in uninstrumented code.
enum MainThreadActivityBreadcrumb {
private final class State: @unchecked Sendable {
let lock = NSLock()
var stack: [String] = []
}
private static let state = State()
static var current: String? {
guard MainThreadHangWatchdog.isEnabledForCurrentProcess else { return nil }
return self.state.lock.withLock { self.state.stack.last }
}
static func push(_ label: String) {
guard MainThreadHangWatchdog.isEnabledForCurrentProcess else { return }
self.state.lock.withLock {
self.state.stack.append(label)
}
}
static func pop() {
guard MainThreadHangWatchdog.isEnabledForCurrentProcess else { return }
self.state.lock.withLock {
_ = self.state.stack.popLast()
}
}
}
/// Detects main-queue response delays and records breadcrumbs for the work that
/// occupied the main thread. Long hangs launch `/usr/bin/sample` asynchronously
/// so sampling cannot delay recovery detection or inflate the reported duration.
final class MainThreadHangWatchdog: @unchecked Sendable {
static let shared = MainThreadHangWatchdog()
static let isEnabledForCurrentProcess: Bool = {
#if DEBUG
true
#else
let environment = ProcessInfo.processInfo.environment
return environment["CODEXBAR_MAIN_THREAD_HANG_WATCHDOG"] == "1" ||
UserDefaults.standard.bool(forKey: "debugMainThreadHangWatchdog")
#endif
}()
private let logger = CodexBarLog.logger(LogCategories.app)
private let pingInterval: TimeInterval
private let hangThreshold: TimeInterval
private let sampleThreshold: TimeInterval
private let sampleCooldown: TimeInterval
private let sampleCaptureOverride: (@Sendable () -> String?)?
private let schedulePing: @Sendable (@escaping @Sendable () -> Void) -> Void
private let lock = NSLock()
private var isRunning = false
private var lastSampleAt: Date?
private var activeSampleProcesses: [ObjectIdentifier: Process] = [:]
var onHangForTesting: ((TimeInterval, [String]) -> Void)?
#if DEBUG
var onHangDetectionForTesting: (() -> Void)?
private var onSampleAttemptForTesting: (() -> Void)?
#endif
private enum SampleCaptureResult {
case coolingDown
case attempted(String?)
}
init(
pingInterval: TimeInterval = 0.025,
hangThreshold: TimeInterval = 0.15,
sampleThreshold: TimeInterval = 2.0,
sampleCooldown: TimeInterval = 300,
sampleCaptureOverride: (@Sendable () -> String?)? = nil,
schedulePing: @escaping @Sendable (@escaping @Sendable () -> Void) -> Void = { response in
DispatchQueue.main.async(execute: response)
})
{
self.pingInterval = pingInterval
self.hangThreshold = hangThreshold
self.sampleThreshold = sampleThreshold
self.sampleCooldown = sampleCooldown
self.sampleCaptureOverride = sampleCaptureOverride
self.schedulePing = schedulePing
}
func start() {
self.lock.lock()
defer { self.lock.unlock() }
guard !self.isRunning else { return }
self.isRunning = true
let thread = Thread { [weak self] in self?.run() }
thread.name = "CodexBar.MainThreadHangWatchdog"
thread.qualityOfService = .utility
thread.start()
}
func stop() {
self.lock.withLock {
self.isRunning = false
}
}
private var shouldRun: Bool {
self.lock.withLock { self.isRunning }
}
private final class PingBox: @unchecked Sendable {
private let lock = NSLock()
private let semaphore = DispatchSemaphore(value: 0)
private var _respondedAt: DispatchTime?
func markResponded() {
self.lock.withLock {
self._respondedAt = .now()
}
self.semaphore.signal()
}
var respondedAt: DispatchTime? {
self.lock.withLock { self._respondedAt }
}
func waitForResponse(timeout: TimeInterval) -> Bool {
self.semaphore.wait(timeout: .now() + timeout) == .success
}
}
private func run() {
while self.shouldRun {
let box = PingBox()
let pingSentAt = DispatchTime.now()
self.schedulePing { box.markResponded() }
if !box.waitForResponse(timeout: self.hangThreshold) {
guard self.shouldRun else { return }
#if DEBUG
self.onHangDetectionForTesting?()
#endif
self.traceHang(box: box, pingSentAt: pingSentAt)
}
guard self.shouldRun else { return }
Thread.sleep(forTimeInterval: self.pingInterval)
}
}
private func traceHang(box: PingBox, pingSentAt: DispatchTime) {
// One delayed ping can span several main-thread operations, so retain each
// distinct breadcrumb observed until the queued ping finally executes.
var activities: [String] = []
func recordActivity() {
guard activities.count < 8,
let activity = MainThreadActivityBreadcrumb.current,
!activities.contains(activity)
else { return }
activities.append(activity)
}
recordActivity()
var sampleFile: String?
var didAttemptSample = false
while box.respondedAt == nil, self.shouldRun {
recordActivity()
if !didAttemptSample, self.elapsedSeconds(since: pingSentAt) >= self.sampleThreshold {
#if DEBUG
self.onSampleAttemptForTesting?()
#endif
switch self.captureSampleIfAllowed() {
case .coolingDown:
break
case let .attempted(file):
didAttemptSample = true
sampleFile = file
}
}
Thread.sleep(forTimeInterval: 0.025)
}
guard let respondedAt = box.respondedAt else { return }
let duration = self.elapsedSeconds(from: pingSentAt, to: respondedAt)
var metadata: [String: String] = [
"durationMs": String(format: "%.0f", duration * 1000),
"activity": activities.isEmpty ? "unknown" : activities.joined(separator: ","),
]
if let sampleFile {
metadata["sampleRequested"] = sampleFile
}
self.logger.warning("main thread hang", metadata: metadata)
self.onHangForTesting?(duration, activities)
}
private func elapsedSeconds(since start: DispatchTime) -> TimeInterval {
self.elapsedSeconds(from: start, to: .now())
}
private func elapsedSeconds(from start: DispatchTime, to end: DispatchTime) -> TimeInterval {
TimeInterval(end.uptimeNanoseconds - start.uptimeNanoseconds) / 1_000_000_000
}
private func captureSampleIfAllowed() -> SampleCaptureResult {
let now = Date()
let shouldCapture = self.lock.withLock {
if self.lastSampleAt.map({ now.timeIntervalSince($0) < self.sampleCooldown }) ?? false {
return false
}
self.lastSampleAt = now
return true
}
guard shouldCapture else { return .coolingDown }
let file = if let sampleCaptureOverride {
sampleCaptureOverride()
} else {
self.launchSample()
}
return .attempted(file)
}
private func launchSample() -> String? {
let directory = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent("Library/Logs/CodexBar", isDirectory: true)
do {
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
} catch {
self.logger.warning(
"main thread hang sample failed",
metadata: ["error": "\(error)"])
return nil
}
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withFullDate, .withTime]
let stamp = formatter.string(from: Date()).replacingOccurrences(of: ":", with: "-")
let file = directory.appendingPathComponent("hang-sample-\(stamp).txt")
let process = Process()
let processID = ObjectIdentifier(process)
process.executableURL = URL(fileURLWithPath: "/usr/bin/sample")
process.arguments = ["\(ProcessInfo.processInfo.processIdentifier)", "3", "-file", file.path]
process.terminationHandler = { [weak self] completedProcess in
self?.sampleDidFinish(completedProcess, processID: processID, file: file)
}
self.lock.withLock {
self.activeSampleProcesses[processID] = process
}
do {
try process.run()
} catch {
_ = self.lock.withLock {
self.activeSampleProcesses.removeValue(forKey: processID)
}
self.logger.warning(
"main thread hang sample failed",
metadata: ["error": "\(error)"])
return nil
}
return file.path
}
private func sampleDidFinish(_ process: Process, processID: ObjectIdentifier, file: URL) {
_ = self.lock.withLock {
self.activeSampleProcesses.removeValue(forKey: processID)
}
guard process.terminationStatus == 0,
FileManager.default.fileExists(atPath: file.path)
else {
self.logger.warning(
"main thread hang sample failed",
metadata: ["status": "\(process.terminationStatus)"])
return
}
self.logger.info(
"main thread hang sample captured",
metadata: ["sample": file.path])
}
#if DEBUG
func traceHangForTesting(
responseDelay: TimeInterval,
waitForSampleAttempt: Bool = false,
responseBeforeTrace: Bool = false)
{
self.lock.withLock {
self.isRunning = true
}
defer {
self.lock.withLock {
self.isRunning = false
}
self.onSampleAttemptForTesting = nil
}
let box = PingBox()
let pingSentAt = DispatchTime.now()
let scheduleResponse = {
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + responseDelay) {
box.markResponded()
}
}
if responseBeforeTrace {
Thread.sleep(forTimeInterval: responseDelay)
box.markResponded()
} else if waitForSampleAttempt {
self.onSampleAttemptForTesting = scheduleResponse
} else {
scheduleResponse()
}
self.traceHang(box: box, pingSentAt: pingSentAt)
}
#endif
}
@@ -0,0 +1,61 @@
import CodexBarCore
import Foundation
import Observation
enum ManagedCodexAccountCoordinatorError: Error, Equatable {
case authenticationInProgress
}
@MainActor
@Observable
final class ManagedCodexAccountCoordinator {
let service: ManagedCodexAccountService
private(set) var isAuthenticatingManagedAccount: Bool = false
private(set) var authenticatingManagedAccountID: UUID?
private(set) var isRemovingManagedAccount: Bool = false
private(set) var removingManagedAccountID: UUID?
var onManagedAccountsDidChange: (@MainActor () -> Void)?
var hasConflictingManagedAccountOperationInFlight: Bool {
self.isAuthenticatingManagedAccount || self.isRemovingManagedAccount
}
init(service: ManagedCodexAccountService = ManagedCodexAccountService()) {
self.service = service
}
func authenticateManagedAccount(
existingAccountID: UUID? = nil,
timeout: TimeInterval = 120)
async throws -> ManagedCodexAccount
{
guard self.isAuthenticatingManagedAccount == false else {
throw ManagedCodexAccountCoordinatorError.authenticationInProgress
}
self.isAuthenticatingManagedAccount = true
self.authenticatingManagedAccountID = existingAccountID
defer {
self.isAuthenticatingManagedAccount = false
self.authenticatingManagedAccountID = nil
}
let account = try await self.service.authenticateManagedAccount(
existingAccountID: existingAccountID,
timeout: timeout)
self.onManagedAccountsDidChange?()
return account
}
func removeManagedAccount(id: UUID) async throws {
self.isRemovingManagedAccount = true
self.removingManagedAccountID = id
defer {
self.isRemovingManagedAccount = false
self.removingManagedAccountID = nil
}
try await self.service.removeManagedAccount(id: id)
self.onManagedAccountsDidChange?()
}
}
@@ -0,0 +1,497 @@
import AppKit
import CodexBarCore
import Foundation
protocol ManagedCodexHomeProducing: Sendable {
func makeHomeURL() -> URL
func validateManagedHomeForDeletion(_ url: URL) throws
}
protocol ManagedCodexLoginRunning: Sendable {
func run(homePath: String, timeout: TimeInterval) async -> CodexLoginRunner.Result
}
protocol ManagedCodexIdentityReading: Sendable {
func loadAccountIdentity(homePath: String) throws -> CodexAuthBackedAccount
}
protocol ManagedCodexWorkspaceResolving: Sendable {
func resolveWorkspaceIdentity(homePath: String, providerAccountID: String) async -> CodexOpenAIWorkspaceIdentity?
func availableWorkspaceIdentities(homePath: String) async -> [CodexOpenAIWorkspaceIdentity]
}
extension ManagedCodexWorkspaceResolving {
func availableWorkspaceIdentities(homePath _: String) async -> [CodexOpenAIWorkspaceIdentity] {
[]
}
}
protocol ManagedCodexWorkspaceSelecting: Sendable {
@MainActor
func selectWorkspace(
email: String,
currentWorkspaceID: String?,
workspaces: [CodexOpenAIWorkspaceIdentity]) async -> CodexOpenAIWorkspaceIdentity?
}
enum ManagedCodexAccountServiceError: Error, Equatable {
case loginFailed(CodexLoginRunner.Result)
case missingEmail
case workspaceSelectionCancelled
case unsafeManagedHome(String)
}
extension ManagedCodexAccountServiceError {
var userFacingMessage: String {
switch self {
case let .loginFailed(result):
CodexLoginAlertPresentation.managedLoginFailureMessage(for: result)
case .missingEmail:
L("managed_login_missing_email")
case .workspaceSelectionCancelled:
L("workspace_selection_cancelled")
case let .unsafeManagedHome(path):
String(format: L("unsafe_managed_home"), path)
}
}
}
struct ManagedCodexHomeFactory: ManagedCodexHomeProducing {
let root: URL
init(root: URL = Self.defaultRootURL(), fileManager: FileManager = .default) {
let standardizedRoot = root.standardizedFileURL
if standardizedRoot.path != root.path {
self.root = standardizedRoot
} else {
self.root = root
}
_ = fileManager
}
func makeHomeURL() -> URL {
self.root.appendingPathComponent(UUID().uuidString, isDirectory: true)
}
func validateManagedHomeForDeletion(_ url: URL) throws {
let rootPath = self.root.standardizedFileURL.path
let targetPath = url.standardizedFileURL.path
let rootPrefix = rootPath.hasSuffix("/") ? rootPath : rootPath + "/"
guard targetPath.hasPrefix(rootPrefix), targetPath != rootPath else {
throw ManagedCodexAccountServiceError.unsafeManagedHome(url.path)
}
}
static func defaultRootURL(fileManager: FileManager = .default) -> URL {
let base = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
?? fileManager.homeDirectoryForCurrentUser
return base
.appendingPathComponent("CodexBar", isDirectory: true)
.appendingPathComponent("managed-codex-homes", isDirectory: true)
}
}
struct DefaultManagedCodexLoginRunner: ManagedCodexLoginRunning {
func run(homePath: String, timeout: TimeInterval) async -> CodexLoginRunner.Result {
await CodexLoginRunner.run(homePath: homePath, timeout: timeout)
}
}
struct DefaultManagedCodexIdentityReader: ManagedCodexIdentityReading {
func loadAccountIdentity(homePath: String) throws -> CodexAuthBackedAccount {
let env = CodexHomeScope.scopedEnvironment(
base: ProcessInfo.processInfo.environment,
codexHome: homePath)
return UsageFetcher(environment: env).loadAuthBackedCodexAccount()
}
}
struct DefaultManagedCodexWorkspaceResolver: ManagedCodexWorkspaceResolving {
private let workspaceCache: CodexOpenAIWorkspaceIdentityCache
init(
workspaceCache: CodexOpenAIWorkspaceIdentityCache = CodexOpenAIWorkspaceIdentityCache())
{
self.workspaceCache = workspaceCache
}
func resolveWorkspaceIdentity(homePath: String, providerAccountID: String) async -> CodexOpenAIWorkspaceIdentity? {
let normalizedProviderAccountID = ManagedCodexAccount.normalizeProviderAccountID(providerAccountID)
?? providerAccountID
let env = CodexHomeScope.scopedEnvironment(
base: ProcessInfo.processInfo.environment,
codexHome: homePath)
if let credentials = try? CodexOAuthCredentialsStore.load(env: env),
let authoritativeIdentity = try? await CodexOpenAIWorkspaceResolver.resolve(credentials: credentials)
{
try? self.workspaceCache.store(authoritativeIdentity)
return authoritativeIdentity
}
let cachedLabel = self.workspaceCache.workspaceLabel(for: normalizedProviderAccountID)
return CodexOpenAIWorkspaceIdentity(
workspaceAccountID: normalizedProviderAccountID,
workspaceLabel: cachedLabel)
}
func availableWorkspaceIdentities(homePath: String) async -> [CodexOpenAIWorkspaceIdentity] {
let env = CodexHomeScope.scopedEnvironment(
base: ProcessInfo.processInfo.environment,
codexHome: homePath)
guard let credentials = try? CodexOAuthCredentialsStore.load(env: env),
let identities = try? await CodexOpenAIWorkspaceResolver.listWorkspaces(credentials: credentials)
else {
return []
}
for identity in identities {
try? self.workspaceCache.store(identity)
}
return identities
}
}
struct CodexWorkspaceAlertSelector: ManagedCodexWorkspaceSelecting {
@MainActor
func selectWorkspace(
email: String,
currentWorkspaceID: String?,
workspaces: [CodexOpenAIWorkspaceIdentity]) async -> CodexOpenAIWorkspaceIdentity?
{
guard workspaces.count > 1 else { return workspaces.first }
let popup = NSPopUpButton(frame: NSRect(x: 0, y: 0, width: 360, height: 26), pullsDown: false)
let sortedWorkspaces = workspaces.sorted { lhs, rhs in
self.workspaceTitle(lhs) < self.workspaceTitle(rhs)
}
for workspace in sortedWorkspaces {
popup.addItem(withTitle: self.workspaceTitle(workspace))
popup.lastItem?.representedObject = workspace.workspaceAccountID
}
if let currentWorkspaceID,
let selectedIndex = sortedWorkspaces.firstIndex(where: { $0.workspaceAccountID == currentWorkspaceID })
{
popup.selectItem(at: selectedIndex)
}
let alert = NSAlert()
alert.messageText = L("Choose Codex workspace")
alert.informativeText = String(format: L("multiple_workspaces_found"), email)
alert.alertStyle = .informational
alert.accessoryView = popup
alert.addButton(withTitle: L("Add Workspace"))
alert.addButton(withTitle: L("Cancel"))
guard alert.runModal() == .alertFirstButtonReturn else {
return nil
}
let selectedWorkspaceID = popup.selectedItem?.representedObject as? String
return sortedWorkspaces.first { $0.workspaceAccountID == selectedWorkspaceID }
}
private func workspaceTitle(_ workspace: CodexOpenAIWorkspaceIdentity) -> String {
workspace.workspaceLabel ?? workspace.workspaceAccountID
}
}
@MainActor
final class ManagedCodexAccountService {
private let store: any ManagedCodexAccountStoring
private let homeFactory: any ManagedCodexHomeProducing
private let loginRunner: any ManagedCodexLoginRunning
private let identityReader: any ManagedCodexIdentityReading
private let workspaceResolver: any ManagedCodexWorkspaceResolving
private let workspaceSelector: any ManagedCodexWorkspaceSelecting
private let fileManager: FileManager
init(
store: any ManagedCodexAccountStoring,
homeFactory: any ManagedCodexHomeProducing,
loginRunner: any ManagedCodexLoginRunning,
identityReader: any ManagedCodexIdentityReading,
workspaceResolver: any ManagedCodexWorkspaceResolving = DefaultManagedCodexWorkspaceResolver(),
workspaceSelector: any ManagedCodexWorkspaceSelecting = CodexWorkspaceAlertSelector(),
fileManager: FileManager = .default)
{
self.store = store
self.homeFactory = homeFactory
self.loginRunner = loginRunner
self.identityReader = identityReader
self.workspaceResolver = workspaceResolver
self.workspaceSelector = workspaceSelector
self.fileManager = fileManager
}
convenience init(fileManager: FileManager = .default) {
self.init(
store: FileManagedCodexAccountStore(fileManager: fileManager),
homeFactory: ManagedCodexHomeFactory(fileManager: fileManager),
loginRunner: DefaultManagedCodexLoginRunner(),
identityReader: DefaultManagedCodexIdentityReader(),
workspaceResolver: DefaultManagedCodexWorkspaceResolver(),
workspaceSelector: CodexWorkspaceAlertSelector(),
fileManager: fileManager)
}
func authenticateManagedAccount(
existingAccountID: UUID? = nil,
timeout: TimeInterval = 120)
async throws -> ManagedCodexAccount
{
let snapshot = try self.store.loadAccounts()
let homeURL = self.homeFactory.makeHomeURL()
try self.fileManager.createDirectory(at: homeURL, withIntermediateDirectories: true)
let account: ManagedCodexAccount
let existingHomePathsToDelete: [String]
do {
let result = await self.loginRunner.run(homePath: homeURL.path, timeout: timeout)
guard case .success = result.outcome else { throw ManagedCodexAccountServiceError.loginFailed(result) }
let identity = try self.identityReader.loadAccountIdentity(homePath: homeURL.path)
guard let rawEmail = identity.email?.trimmingCharacters(in: .whitespacesAndNewlines),
!rawEmail.isEmpty
else {
throw ManagedCodexAccountServiceError.missingEmail
}
let authenticatedProviderAccountID: String? = switch identity.identity {
case let .providerAccount(id):
ManagedCodexAccount.normalizeProviderAccountID(id)
case .emailOnly, .unresolved:
nil
}
let selectedWorkspace = try await self.selectedWorkspaceIdentity(
email: rawEmail,
homePath: homeURL.path,
authenticatedProviderAccountID: authenticatedProviderAccountID)
let providerAccountID = selectedWorkspace?.workspaceAccountID ?? authenticatedProviderAccountID
let workspaceIdentity: CodexOpenAIWorkspaceIdentity? = if let selectedWorkspace {
selectedWorkspace
} else {
await self.resolvedWorkspaceIdentity(
homePath: homeURL.path,
providerAccountID: providerAccountID)
}
let now = Date().timeIntervalSince1970
let existing = self.reconciledExistingAccount(
authenticatedEmail: rawEmail,
providerAccountID: providerAccountID,
existingAccountID: existingAccountID,
snapshot: snapshot)
let persistedMetadata = self.persistedProviderMetadata(
authenticatedProviderAccountID: providerAccountID,
resolvedWorkspaceIdentity: workspaceIdentity,
existingAccount: existing)
account = ManagedCodexAccount(
id: existing?.id ?? UUID(),
email: rawEmail,
providerAccountID: persistedMetadata.providerAccountID,
workspaceLabel: persistedMetadata.workspaceLabel,
workspaceAccountID: persistedMetadata.workspaceAccountID,
authFingerprint: CodexAuthFingerprint.fingerprint(
homePath: homeURL.path,
fileManager: self.fileManager),
managedHomePath: homeURL.path,
createdAt: existing?.createdAt ?? now,
updatedAt: now,
lastAuthenticatedAt: now)
let replacedAccountIDs = self.replacedAccountIDs(
authenticatedEmail: rawEmail,
providerAccountID: providerAccountID,
existingAccountID: existingAccountID,
matchedAccountID: existing?.id,
snapshot: snapshot)
existingHomePathsToDelete = snapshot.accounts
.filter { replacedAccountIDs.contains($0.id) }
.map(\.managedHomePath)
let updatedSnapshot = ManagedCodexAccountSet(
version: snapshot.version,
accounts: snapshot.accounts.filter { replacedAccountIDs.contains($0.id) == false } + [account])
try self.store.storeAccounts(updatedSnapshot)
} catch {
try? self.removeManagedHomeIfSafe(atPath: homeURL.path)
throw error
}
for existingHomePathToDelete in existingHomePathsToDelete where existingHomePathToDelete != homeURL.path {
try? self.removeManagedHomeIfSafe(atPath: existingHomePathToDelete)
}
return account
}
func removeManagedAccount(id: UUID) async throws {
let snapshot = try self.store.loadAccounts()
guard let account = snapshot.account(id: id) else { return }
let homeURL = URL(fileURLWithPath: account.managedHomePath, isDirectory: true)
let canDeleteHome = (try? self.homeFactory.validateManagedHomeForDeletion(homeURL)) != nil
let remaining = snapshot.accounts.filter { $0.id != id }
try self.store.storeAccounts(ManagedCodexAccountSet(
version: snapshot.version,
accounts: remaining))
if canDeleteHome, self.fileManager.fileExists(atPath: homeURL.path) {
try? self.fileManager.removeItem(at: homeURL)
}
}
private func removeManagedHomeIfSafe(atPath path: String) throws {
let homeURL = URL(fileURLWithPath: path, isDirectory: true)
try self.homeFactory.validateManagedHomeForDeletion(homeURL)
if self.fileManager.fileExists(atPath: homeURL.path) {
try self.fileManager.removeItem(at: homeURL)
}
}
private func selectedWorkspaceIdentity(
email: String,
homePath: String,
authenticatedProviderAccountID: String?) async throws -> CodexOpenAIWorkspaceIdentity?
{
let workspaces = await self.workspaceResolver.availableWorkspaceIdentities(homePath: homePath)
guard workspaces.count > 1 else {
return workspaces.first { $0.workspaceAccountID == authenticatedProviderAccountID }
}
guard let selected = await self.workspaceSelector.selectWorkspace(
email: email,
currentWorkspaceID: authenticatedProviderAccountID,
workspaces: workspaces)
else {
throw ManagedCodexAccountServiceError.workspaceSelectionCancelled
}
try self.persistSelectedWorkspaceID(selected.workspaceAccountID, homePath: homePath)
return selected
}
private func resolvedWorkspaceIdentity(
homePath: String,
providerAccountID: String?) async -> CodexOpenAIWorkspaceIdentity?
{
guard let providerAccountID else { return nil }
return await self.workspaceResolver.resolveWorkspaceIdentity(
homePath: homePath,
providerAccountID: providerAccountID)
}
private func persistSelectedWorkspaceID(_ workspaceID: String, homePath: String) throws {
let env = CodexHomeScope.scopedEnvironment(
base: ProcessInfo.processInfo.environment,
codexHome: homePath)
let credentials = try CodexOAuthCredentialsStore.load(env: env)
try CodexOAuthCredentialsStore.save(
CodexOAuthCredentials(
accessToken: credentials.accessToken,
refreshToken: credentials.refreshToken,
idToken: credentials.idToken,
accountId: workspaceID,
lastRefresh: credentials.lastRefresh),
env: env)
}
private func reconciledExistingAccount(
authenticatedEmail: String,
providerAccountID: String?,
existingAccountID: UUID?,
snapshot: ManagedCodexAccountSet)
-> ManagedCodexAccount?
{
if let providerAccountID,
let existingByProviderAccountID = snapshot.account(
email: authenticatedEmail,
providerAccountID: providerAccountID)
{
return existingByProviderAccountID
}
if let existingAccountID,
let existingByID = snapshot.account(id: existingAccountID),
existingByID.email == Self.normalizeEmail(authenticatedEmail),
providerAccountID == nil || existingByID.providerAccountID == nil
{
return existingByID
}
guard providerAccountID == nil else {
return nil
}
// Email-only reconciliation is a legacy/hardening fallback. Once an auth payload carries a
// provider account ID, matching must stay on that ID so same-email workspaces can coexist.
return snapshot.account(email: authenticatedEmail)
}
private func replacedAccountIDs(
authenticatedEmail: String,
providerAccountID: String?,
existingAccountID: UUID?,
matchedAccountID: UUID?,
snapshot: ManagedCodexAccountSet) -> Set<UUID>
{
var ids: Set<UUID> = []
let normalizedEmail = Self.normalizeEmail(authenticatedEmail)
if let matchedAccountID {
ids.insert(matchedAccountID)
}
if providerAccountID != nil {
let legacySameEmailIDs = snapshot.accounts
.filter {
$0.id != matchedAccountID &&
$0.providerAccountID == nil &&
$0.email == normalizedEmail
}
.map(\.id)
ids.formUnion(legacySameEmailIDs)
}
guard let existingAccountID,
existingAccountID != matchedAccountID,
let existingByID = snapshot.account(id: existingAccountID)
else {
return ids
}
if existingByID.providerAccountID == nil,
existingByID.email == normalizedEmail,
providerAccountID != nil
{
ids.insert(existingAccountID)
}
return ids
}
private static func normalizeEmail(_ email: String) -> String {
email.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}
private func persistedProviderMetadata(
authenticatedProviderAccountID: String?,
resolvedWorkspaceIdentity: CodexOpenAIWorkspaceIdentity?,
existingAccount: ManagedCodexAccount?) -> (
providerAccountID: String?,
workspaceLabel: String?,
workspaceAccountID: String?)
{
if let authenticatedProviderAccountID {
let isExistingProviderMatch = existingAccount?.providerAccountID == authenticatedProviderAccountID
return (
providerAccountID: authenticatedProviderAccountID,
workspaceLabel: resolvedWorkspaceIdentity?.workspaceLabel
?? (isExistingProviderMatch ? existingAccount?.workspaceLabel : nil),
workspaceAccountID: resolvedWorkspaceIdentity?.workspaceAccountID ??
(isExistingProviderMatch ? existingAccount?.workspaceAccountID : nil) ??
authenticatedProviderAccountID)
}
guard let existingAccount, existingAccount.providerAccountID != nil else {
return (providerAccountID: nil, workspaceLabel: nil, workspaceAccountID: nil)
}
return (
providerAccountID: existingAccount.providerAccountID,
workspaceLabel: existingAccount.workspaceLabel,
workspaceAccountID: existingAccount.workspaceAccountID ?? existingAccount.providerAccountID)
}
}
@@ -0,0 +1,149 @@
import CodexBarCore
import Dispatch
import Foundation
@MainActor
struct MemoryPressureCacheTrimSummary: Equatable {
var menuCardHeights = 0
var menuWidths = 0
var mergedSwitcherSelections = 0
var recycledMenuCardViews = 0
var openAIWebDebugLines = 0
var total: Int {
self.menuCardHeights +
self.menuWidths +
self.mergedSwitcherSelections +
self.recycledMenuCardViews +
self.openAIWebDebugLines
}
var metadata: [String: String] {
[
"menuCardHeights": "\(self.menuCardHeights)",
"menuWidths": "\(self.menuWidths)",
"mergedSwitcherSelections": "\(self.mergedSwitcherSelections)",
"recycledMenuCardViews": "\(self.recycledMenuCardViews)",
"openAIWebDebugLines": "\(self.openAIWebDebugLines)",
"total": "\(self.total)",
]
}
mutating func merge(_ other: MemoryPressureCacheTrimSummary) {
self.menuCardHeights += other.menuCardHeights
self.menuWidths += other.menuWidths
self.mergedSwitcherSelections += other.mergedSwitcherSelections
self.recycledMenuCardViews += other.recycledMenuCardViews
self.openAIWebDebugLines += other.openAIWebDebugLines
}
}
@MainActor
final class MemoryPressureMonitor {
typealias CacheTrimHandler = @MainActor () -> MemoryPressureCacheTrimSummary
private let logger = CodexBarLog.logger(LogCategories.memoryPressure)
private let releaseFreeMallocPages: @Sendable () -> Void
private let trimAppCaches: CacheTrimHandler
private var source: DispatchSourceMemoryPressure?
init(
trimAppCaches: @escaping CacheTrimHandler = { MemoryPressureCacheTrimSummary() },
releaseFreeMallocPages: @escaping @Sendable () -> Void = {
MemoryPressureRelief.releaseFreeMallocPages()
})
{
self.trimAppCaches = trimAppCaches
self.releaseFreeMallocPages = releaseFreeMallocPages
}
func start() {
guard self.source == nil else { return }
let source = DispatchSource.makeMemoryPressureSource(
eventMask: [.warning, .critical],
queue: .global(qos: .utility))
source.setEventHandler(handler: Self.makeEventHandler(
source: source,
handle: { [weak self] isWarning, isCritical in
self?.handleMemoryPressure(isWarning: isWarning, isCritical: isCritical)
}))
self.source = source
source.resume()
}
nonisolated static func makeEventHandler(
source: DispatchSourceMemoryPressure,
handle: @escaping @MainActor @Sendable (_ isWarning: Bool, _ isCritical: Bool) -> Void)
-> @Sendable () -> Void
{
self.makeEventHandler(
eventReader: { [weak source] in source?.data ?? [] },
handle: handle)
}
nonisolated static func makeEventHandler(
eventReader: @escaping @Sendable () -> DispatchSource.MemoryPressureEvent,
handle: @escaping @MainActor @Sendable (_ isWarning: Bool, _ isCritical: Bool) -> Void)
-> @Sendable () -> Void
{
// DispatchSource invokes this on the utility queue. Keep the handler
// nonisolated, then hop to MainActor for app-state cleanup.
{ @Sendable in
let event = eventReader()
let isWarning = event.contains(.warning)
let isCritical = event.contains(.critical)
Task { @MainActor in
handle(isWarning, isCritical)
}
}
}
func stop() {
self.source?.cancel()
self.source = nil
}
deinit {
self.source?.cancel()
}
#if DEBUG
func handleMemoryPressureForTesting(isWarning: Bool, isCritical: Bool) {
self.handleMemoryPressure(isWarning: isWarning, isCritical: isCritical)
}
#endif
private func handleMemoryPressure(isWarning: Bool, isCritical: Bool) {
let level = if isCritical {
"critical"
} else if isWarning {
"warning"
} else {
"normal"
}
self.logger.warning("System memory pressure", metadata: ["level": level])
#if DEBUG
let cachedWebViewsBefore = OpenAIDashboardFetcher.cachedWebViewCountForTesting()
#endif
OpenAIDashboardFetcher.evictIdleCachedWebViews()
#if DEBUG
let cachedWebViewsAfter = OpenAIDashboardFetcher.cachedWebViewCountForTesting()
self.logger.info(
"Memory pressure OpenAI webview cache",
metadata: [
"before": "\(cachedWebViewsBefore)",
"after": "\(cachedWebViewsAfter)",
"evicted": "\(max(0, cachedWebViewsBefore - cachedWebViewsAfter))",
])
#endif
let trimSummary = self.trimAppCaches()
if trimSummary.total > 0 {
self.logger.info("Trimmed app caches for memory pressure", metadata: trimSummary.metadata)
}
let releaseFreeMallocPages = self.releaseFreeMallocPages
Task.detached(priority: .utility) {
releaseFreeMallocPages()
}
}
}
@@ -0,0 +1,7 @@
import Darwin
enum MemoryPressureRelief {
static func releaseFreeMallocPages() {
_ = malloc_zone_pressure_relief(nil, 0)
}
}
+31
View File
@@ -0,0 +1,31 @@
import Foundation
/// Controls what the menu bar displays when brand icon mode is enabled.
enum MenuBarDisplayMode: String, CaseIterable, Identifiable {
case percent
case pace
case both
case resetTime
var id: String {
self.rawValue
}
var label: String {
switch self {
case .percent: L("display_mode_percent")
case .pace: L("display_mode_pace")
case .both: L("display_mode_both")
case .resetTime: L("display_mode_reset_time")
}
}
var description: String {
switch self {
case .percent: L("display_mode_percent_desc")
case .pace: L("display_mode_pace_desc")
case .both: L("display_mode_both_desc")
case .resetTime: L("display_mode_reset_time_desc")
}
}
}
+182
View File
@@ -0,0 +1,182 @@
import CodexBarCore
import Foundation
enum MenuBarDisplayText {
static func percentText(window: RateWindow?, showUsed: Bool) -> String? {
guard let window else { return nil }
let percent = showUsed ? window.usedPercent : window.remainingPercent
return UsageFormatter.percentString(percent)
}
static func paceText(pace: UsagePace?) -> String? {
guard let pace else { return nil }
let deltaValue = Int(abs(pace.deltaPercent).rounded())
if deltaValue == 0 { return "0%" }
let sign = pace.deltaPercent >= 0 ? "+" : "-"
return "\(sign)\(deltaValue)%"
}
/// Combined "session · weekly" menu-bar text shared by providers that expose both a
/// session (5h) and weekly (7d) lane, e.g. Codex and Claude.
static func combinedSessionWeeklyPercentText(
sessionWindow: RateWindow?,
weeklyWindow: RateWindow?,
showUsed: Bool,
resetTimeDisplayStyle: ResetTimeDisplayStyle = .countdown,
showsResetTimeWhenExhausted: Bool = false,
now: Date = .init())
-> String?
{
var parts: [String] = []
if let sessionWindow,
let session = self.laneValueText(
window: sessionWindow,
showUsed: showUsed,
resetTimeDisplayStyle: resetTimeDisplayStyle,
showsResetTimeWhenExhausted: showsResetTimeWhenExhausted,
now: now)
{
parts.append("\(self.sessionWindowLabel(window: sessionWindow)) \(session)")
}
if let weeklyWindow,
let weekly = self.laneValueText(
window: weeklyWindow,
showUsed: showUsed,
resetTimeDisplayStyle: resetTimeDisplayStyle,
showsResetTimeWhenExhausted: showsResetTimeWhenExhausted,
now: now)
{
parts.append("W \(weekly)")
}
return parts.isEmpty ? nil : parts.joined(separator: " · ")
}
private static func laneValueText(
window: RateWindow,
showUsed: Bool,
resetTimeDisplayStyle: ResetTimeDisplayStyle,
showsResetTimeWhenExhausted: Bool,
now: Date) -> String?
{
if let resetText = self.exhaustedResetText(
window: window,
enabled: showsResetTimeWhenExhausted,
style: resetTimeDisplayStyle,
now: now)
{
return resetText
}
return self.percentText(window: window, showUsed: showUsed)
}
private static func sessionWindowLabel(window: RateWindow) -> String {
guard let minutes = window.windowMinutes, minutes > 0 else { return "S" }
guard minutes.isMultiple(of: 60) else { return "\(minutes)m" }
return "\(minutes / 60)h"
}
static func displayText(
mode: MenuBarDisplayMode,
percentWindow: RateWindow?,
pace: UsagePace? = nil,
showUsed: Bool,
resetTimeDisplayStyle: ResetTimeDisplayStyle = .countdown,
showsResetTimeWhenExhausted: Bool = false,
now: Date = .init()) -> String?
{
if mode != .resetTime,
showsResetTimeWhenExhausted,
let percentWindow,
percentWindow.remainingPercent <= 0
{
if let resetText = self.exhaustedResetText(
window: percentWindow,
enabled: true,
style: resetTimeDisplayStyle,
now: now)
{
return resetText
}
// Smart mode cannot replace an exhausted percentage unless the reset is concrete, future,
// and schedulable. Preserve the quota signal in pace/both modes too; a pace from another
// combined lane must not hide that this displayed lane is already exhausted.
return self.percentText(window: percentWindow, showUsed: showUsed)
}
switch mode {
case .percent:
return self.percentText(window: percentWindow, showUsed: showUsed)
case .pace:
// Pace can be temporarily unavailable near a reset or when a provider omits window metadata.
// Keep the selected quota visible instead of collapsing the status item to an icon-only state.
return self.paceText(pace: pace)
?? self.percentText(window: percentWindow, showUsed: showUsed)
case .both:
guard let percent = percentText(window: percentWindow, showUsed: showUsed) else { return nil }
// Fall back to percent-only when pace is unavailable (e.g. Copilot)
guard let paceText = Self.paceText(pace: pace) else { return percent }
return "\(percent) · \(paceText)"
case .resetTime:
guard let percentWindow else { return nil }
return self.resetTimeText(window: percentWindow, style: resetTimeDisplayStyle, now: now)
?? self.percentText(window: percentWindow, showUsed: showUsed)
}
}
/// " " reset text for a window, or nil when it carries no usable reset metadata.
static func resetTimeText(
window: RateWindow,
style: ResetTimeDisplayStyle,
now: Date) -> String?
{
if let resetsAt = window.resetsAt {
let description = switch style {
case .countdown:
UsageFormatter.resetCountdownDescription(from: resetsAt, now: now)
case .absolute:
UsageFormatter.resetDescription(from: resetsAt, now: now)
}
return "\(description)"
}
if let resetDescription = self.resetMetadataText(window.resetDescription) {
return "\(resetDescription)"
}
return nil
}
/// Smart-mode replacement: when enabled and the quota is exhausted (0% remaining, regardless of
/// whether the display shows used or remaining), surface the reset time instead of a dead percent.
///
/// Requires a concrete, still-future `resetsAt`. The smart option only replaces the percent when it
/// has a reset time it can both render as a live countdown/clock AND hand to the refresh scheduler,
/// so the lane keeps ticking and flips back to the percentage once the reset passes. Windows with
/// only textual reset metadata (`resetDescription`, no `resetsAt`) or an already-elapsed reset can't
/// be scheduled, so they keep showing the percent instead of freezing on stale reset text.
private static func exhaustedResetText(
window: RateWindow?,
enabled: Bool,
style: ResetTimeDisplayStyle,
now: Date) -> String?
{
guard enabled, let window, window.remainingPercent <= 0 else { return nil }
guard let resetsAt = window.resetsAt, resetsAt > now else { return nil }
return self.resetTimeText(window: window, style: style, now: now)
}
private static func resetMetadataText(_ description: String?) -> String? {
guard let description else { return nil }
let trimmed = description.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
// RateWindow.resetDescription predates provider-specific detail fields and is also used for
// request/token summaries. Only trust phrases that explicitly describe reset timing.
let normalized = trimmed.lowercased()
let resetPrefixes = [
"reset ", "resets ", "in ", "today ", "today,", "tomorrow ", "tomorrow,", "next ",
"expire ", "expires ", "refill ", "refills ",
]
let exactResetDescriptions = ["today", "tomorrow", "expired", "now", "soon"]
return exactResetDescriptions.contains(normalized) || resetPrefixes.contains(where: normalized.hasPrefix)
? trimmed
: nil
}
}
@@ -0,0 +1,283 @@
import CodexBarCore
import Foundation
enum MenuBarMetricWindowResolver {
private enum Lane {
case primary
case secondary
case tertiary
}
static func rateWindow(
preference: MenuBarMetricPreference,
provider: UsageProvider,
snapshot: UsageSnapshot?,
supportsAverage: Bool)
-> RateWindow?
{
guard let snapshot else { return nil }
switch preference {
case .monthlyPlan:
return snapshot.extraRateWindows?.first { $0.id == "mistral-monthly-plan" }?.window
case .extraUsage:
return Self.extraUsageWindow(snapshot: snapshot)
case .tertiary:
return Self.requestedWindow(
provider: provider,
snapshot: snapshot,
lanes: Self.tertiaryOrder(for: provider))
case .primary:
return Self.requestedWindow(
provider: provider,
snapshot: snapshot,
lanes: Self.primaryOrder(for: provider))
case .secondary:
return Self.requestedWindow(
provider: provider,
snapshot: snapshot,
lanes: Self.secondaryOrder(for: provider))
case .primaryAndSecondary:
// Claude accounts that only expose an enterprise/extra-usage spend limit have no real
// session/weekly lanes; surface the spend limit (as `.automatic` does) instead of an empty
// or 0% placeholder lane.
if provider == .claude, let spendLimit = Self.claudeSpendLimitWindow(snapshot: snapshot) {
return spendLimit
}
return Self.mostConstrainedWindow(
primary: snapshot.primary,
secondary: snapshot.secondary,
tertiary: nil)
case .average:
return Self.averageWindow(provider: provider, snapshot: snapshot, supportsAverage: supportsAverage)
case .automatic:
return Self.automaticWindow(provider: provider, snapshot: snapshot)
}
}
private static func tertiaryOrder(for provider: UsageProvider) -> [Lane] {
if provider == .zai {
return [.tertiary, .primary, .secondary]
}
if provider == .perplexity || provider == .cursor || provider == .antigravity {
return [.tertiary, .secondary, .primary]
}
return [.primary, .secondary]
}
private static func primaryOrder(for provider: UsageProvider) -> [Lane] {
if provider == .zai {
return [.primary, .tertiary, .secondary]
}
if provider == .perplexity || provider == .antigravity {
return [.primary, .secondary, .tertiary]
}
return [.primary, .secondary]
}
private static func secondaryOrder(for provider: UsageProvider) -> [Lane] {
if provider == .zai || provider == .antigravity {
return [.secondary, .primary, .tertiary]
}
if provider == .perplexity {
return [.secondary, .tertiary, .primary]
}
return [.secondary, .primary]
}
private static func averageWindow(
provider: UsageProvider,
snapshot: UsageSnapshot,
supportsAverage: Bool)
-> RateWindow?
{
guard supportsAverage,
let primary = snapshot.primary,
let secondary = snapshot.secondary
else {
if provider == .antigravity {
return self.window(in: snapshot, following: [.primary, .secondary, .tertiary])
}
return snapshot.primary ?? snapshot.secondary
}
let usedPercent = (primary.usedPercent + secondary.usedPercent) / 2
return RateWindow(usedPercent: usedPercent, windowMinutes: nil, resetsAt: nil, resetDescription: nil)
}
private static func automaticWindow(provider: UsageProvider, snapshot: UsageSnapshot) -> RateWindow? {
if provider == .antigravity {
if let window = mostConstrainedAntigravityQuotaSummaryWindow(snapshot: snapshot) {
return window
}
return self.mostConstrainedWindow(
primary: snapshot.primary,
secondary: snapshot.secondary,
tertiary: snapshot.tertiary)
?? self.mostConstrainedAntigravityLegacyExtraWindow(snapshot: snapshot)
}
if provider == .perplexity {
return snapshot.automaticPerplexityWindow()
}
if provider == .zai {
return self.mostConstrainedWindow(
primary: snapshot.primary,
secondary: snapshot.tertiary,
tertiary: nil) ?? snapshot.secondary
}
if provider == .factory || provider == .kimi {
return snapshot.secondary ?? snapshot.primary
}
if provider == .litellm {
return snapshot.secondary ?? snapshot.primary
}
if provider == .copilot,
let primary = snapshot.primary,
let secondary = snapshot.secondary
{
return primary.usedPercent >= secondary.usedPercent ? primary : secondary
}
if provider == .cursor {
return Self.mostConstrainedCursorWindow(
total: snapshot.primary,
auto: snapshot.secondary,
api: snapshot.tertiary)
}
if provider == .minimax {
return Self.mostConstrainedWindow(
primary: snapshot.primary,
secondary: snapshot.secondary,
tertiary: snapshot.tertiary)
}
if provider == .claude, let spendLimit = Self.claudeSpendLimitWindow(snapshot: snapshot) {
return spendLimit
}
return snapshot.primary ?? snapshot.secondary
}
private static let antigravityQuotaSummaryWindowIDPrefix = "antigravity-quota-summary-"
private static let antigravityCompactFallbackWindowIDPrefix = "antigravity-compact-fallback-"
private static func mostConstrainedAntigravityQuotaSummaryWindow(snapshot: UsageSnapshot) -> RateWindow? {
let windows = snapshot.extraRateWindows?
.filter { $0.usageKnown && $0.id.hasPrefix(Self.antigravityQuotaSummaryWindowIDPrefix) }
.map(\.window) ?? []
guard !windows.isEmpty else { return nil }
let usableWindows = windows.filter { $0.usedPercent < 100 }
if let maxUsable = usableWindows.max(by: { $0.usedPercent < $1.usedPercent }) {
return maxUsable
}
return windows.max(by: { $0.usedPercent < $1.usedPercent })
}
private static func mostConstrainedAntigravityLegacyExtraWindow(snapshot: UsageSnapshot) -> RateWindow? {
let windows = snapshot.extraRateWindows?
.filter {
$0.usageKnown && $0.id.hasPrefix(Self.antigravityCompactFallbackWindowIDPrefix)
}
.map(\.window) ?? []
guard !windows.isEmpty else { return nil }
let usableWindows = windows.filter { $0.usedPercent < 100 }
if let maxUsable = usableWindows.max(by: { $0.usedPercent < $1.usedPercent }) {
return maxUsable
}
return windows.max(by: { $0.usedPercent < $1.usedPercent })
}
private static func requestedWindow(
provider: UsageProvider,
snapshot: UsageSnapshot,
lanes: [Lane]) -> RateWindow?
{
self.window(in: snapshot, following: lanes)
?? (provider == .antigravity
? self.mostConstrainedAntigravityLegacyExtraWindow(snapshot: snapshot)
: nil)
}
private static func window(in snapshot: UsageSnapshot, following lanes: [Lane]) -> RateWindow? {
for lane in lanes {
if let window = self.window(in: snapshot, lane: lane) {
return window
}
}
return nil
}
private static func window(in snapshot: UsageSnapshot, lane: Lane) -> RateWindow? {
switch lane {
case .primary:
snapshot.primary
case .secondary:
snapshot.secondary
case .tertiary:
snapshot.tertiary
}
}
private static func mostConstrainedWindow(
primary: RateWindow?,
secondary: RateWindow?,
tertiary: RateWindow?)
-> RateWindow?
{
let windows = [primary, secondary, tertiary].compactMap(\.self)
guard !windows.isEmpty else { return nil }
return windows.max(by: { $0.usedPercent < $1.usedPercent })
}
private static func mostConstrainedCursorWindow(
total: RateWindow?,
auto: RateWindow?,
api: RateWindow?)
-> RateWindow?
{
if let total, total.usedPercent >= 100 {
return total
}
let subquotaWindows = [auto, api].compactMap(\.self)
let usableSubquotaWindows = subquotaWindows.filter { $0.usedPercent < 100 }
if !subquotaWindows.isEmpty, usableSubquotaWindows.isEmpty {
return subquotaWindows.max(by: { $0.usedPercent < $1.usedPercent })
}
return ([total].compactMap(\.self) + usableSubquotaWindows)
.max(by: { $0.usedPercent < $1.usedPercent })
}
/// The Claude spend-limit window when the account only exposes an enterprise/extra-usage spend limit
/// and has no real session/weekly quota lanes (`primary` nil, a `.spendLimit` window, or an explicitly
/// marked placeholder). Lets the automatic and combined metrics surface the spend limit instead of an empty
/// or 0% placeholder lane. Returns nil for accounts that expose genuine quota lanes.
static func claudeSpendLimitWindow(snapshot: UsageSnapshot) -> RateWindow? {
guard self.shouldUseClaudeSpendLimit(providerCost: snapshot.providerCost, snapshot: snapshot) else {
return nil
}
return self.extraUsageWindow(snapshot: snapshot)
}
private static func shouldUseClaudeSpendLimit(
providerCost: ProviderCostSnapshot?,
snapshot: UsageSnapshot)
-> Bool
{
guard providerCost?.limit ?? 0 > 0,
snapshot.secondary == nil,
snapshot.tertiary == nil
else { return false }
guard let primary = snapshot.primary else { return true }
return primary.isSyntheticPlaceholder
}
private static func extraUsageWindow(snapshot: UsageSnapshot?) -> RateWindow? {
guard let cost = snapshot?.providerCost, cost.limit > 0 else { return nil }
let usedPercent = max(0, min(100, (cost.used / cost.limit) * 100))
return RateWindow(
usedPercent: usedPercent,
windowMinutes: nil,
resetsAt: cost.resetsAt,
resetDescription: nil)
}
}
@@ -0,0 +1,54 @@
import Foundation
enum MenuBarStatusItemDefaultsRepair {
static let didRepairKey = "hasRepairedHiddenStatusItemVisibilityDefaults"
private static let visibilityPrefix = "NSStatusItem VisibleCC "
private static let legacyAutosavePrefix = "codexbar-"
static func repairHiddenVisibilityDefaultsIfNeeded(defaults: UserDefaults) -> [String] {
guard !defaults.bool(forKey: self.didRepairKey) else { return [] }
let repairedKeys = defaults.dictionaryRepresentation().keys
.filter { key in
self.shouldRepair(key: key, value: defaults.object(forKey: key))
}
.sorted()
for key in repairedKeys {
defaults.removeObject(forKey: key)
}
defaults.set(true, forKey: self.didRepairKey)
return repairedKeys
}
static func shouldRepair(key: String, value: Any?) -> Bool {
guard key.hasPrefix(self.visibilityPrefix), self.isFalse(value) else { return false }
let itemName = String(key.dropFirst(self.visibilityPrefix.count))
return itemName.hasPrefix(self.legacyAutosavePrefix) || self.isDefaultStatusItemName(itemName)
}
static func visibilityDefault(defaults: UserDefaults, autosaveName: String) -> Bool? {
guard !autosaveName.isEmpty else { return nil }
return self.boolValue(defaults.object(forKey: self.visibilityPrefix + autosaveName))
}
private static func isDefaultStatusItemName(_ itemName: String) -> Bool {
guard itemName.hasPrefix("Item-") else { return false }
return itemName.dropFirst("Item-".count).allSatisfy(\.isNumber)
}
private static func isFalse(_ value: Any?) -> Bool {
self.boolValue(value) == false
}
private static func boolValue(_ value: Any?) -> Bool? {
switch value {
case let number as NSNumber:
number.boolValue
case let bool as Bool:
bool
default:
nil
}
}
}
@@ -0,0 +1,61 @@
import AppKit
@MainActor
enum MenuBarStatusItemPlacementPreflight {
static let preferredPositionPrefix = "NSStatusItem Preferred Position "
static let suspiciousPreferredPositionPadding: Double = 512
static func preferredPositionKey(autosaveName: String) -> String {
"\(self.preferredPositionPrefix)\(autosaveName)"
}
@discardableResult
static func prepare(
defaults: UserDefaults,
autosaveName: String,
legacyDefaultItemIndex: Int? = nil,
maximumPreferredPosition: Double? = currentMaximumPreferredPosition())
-> Bool
{
let key = self.preferredPositionKey(autosaveName: autosaveName)
var repaired = self.clearPreferredPositionIfNeeded(
defaults: defaults,
key: key,
maximumPreferredPosition: maximumPreferredPosition)
if let legacyDefaultItemIndex {
let legacyKey = self.preferredPositionKey(autosaveName: "Item-\(legacyDefaultItemIndex)")
repaired = self.clearPreferredPositionIfNeeded(
defaults: defaults,
key: legacyKey,
maximumPreferredPosition: maximumPreferredPosition) || repaired
}
return repaired
}
static func shouldClearPreferredPosition(_ value: Any, maximumPreferredPosition: Double?) -> Bool {
guard let number = value as? NSNumber else { return true }
let position = number.doubleValue
if position <= 0 {
return true
}
guard let maximumPreferredPosition else { return false }
return position > maximumPreferredPosition + self.suspiciousPreferredPositionPadding
}
private static func clearPreferredPositionIfNeeded(
defaults: UserDefaults,
key: String,
maximumPreferredPosition: Double?)
-> Bool
{
guard let value = defaults.object(forKey: key),
self.shouldClearPreferredPosition(value, maximumPreferredPosition: maximumPreferredPosition)
else { return false }
defaults.removeObject(forKey: key)
return true
}
private static func currentMaximumPreferredPosition() -> Double? {
NSScreen.screens.map { Double($0.frame.maxX) }.max()
}
}
@@ -0,0 +1,110 @@
import AppKit
import CoreGraphics
import Foundation
struct MenuBarStatusItemWindowSnapshot: Equatable, CustomStringConvertible {
let name: String
let ownerName: String
let bounds: CGRect
let isOnscreen: Bool
let displayBounds: CGRect?
var isWithinDisplayBounds: Bool {
guard let displayBounds else { return false }
return displayBounds.contains(self.bounds)
}
var isTahoeBlockedProxy: Bool {
self.ownerName == "Control Center"
&& self.isOnscreen
&& abs(self.bounds.minX) <= 1
&& self.bounds.maxY <= 0
&& self.bounds.width > 0
&& self.bounds.height > 0
&& !self.isWithinDisplayBounds
}
var description: String {
let display = self.displayBounds.map {
"display=\(Int($0.minX)),\(Int($0.minY)) \(Int($0.width))x\(Int($0.height))"
} ?? "display=nil"
return "name=\(self.name),owner=\(self.ownerName),x=\(Int(self.bounds.minX)),"
+ "w=\(Int(self.bounds.width)),onscreen=\(self.isOnscreen),"
+ "withinDisplay=\(self.isWithinDisplayBounds),\(display)"
}
}
enum MenuBarStatusItemWindowProbe {
static func snapshots(matching names: Set<String>) -> [MenuBarStatusItemWindowSnapshot] {
self.snapshots(
matching: names,
windowInfo: self.windowInfo(),
displayBounds: NSScreen.screens.map(\.frame))
}
static func snapshots(
matching names: Set<String>,
windowInfo: [[String: Any]],
displayBounds: [CGRect])
-> [MenuBarStatusItemWindowSnapshot]
{
guard !names.isEmpty else { return [] }
return windowInfo.compactMap { record in
self.snapshot(record: record, matching: names, displayBounds: displayBounds)
}
}
private static func windowInfo() -> [[String: Any]] {
guard let windows = CGWindowListCopyWindowInfo([.optionAll], kCGNullWindowID) as? [[String: Any]] else {
return []
}
return windows
}
private static func snapshot(
record: [String: Any],
matching names: Set<String>,
displayBounds: [CGRect])
-> MenuBarStatusItemWindowSnapshot?
{
guard let name = record[kCGWindowName as String] as? String,
names.contains(name),
let bounds = self.bounds(record[kCGWindowBounds as String])
else { return nil }
let ownerName = record[kCGWindowOwnerName as String] as? String ?? "unknown"
let isOnscreen = (record[kCGWindowIsOnscreen as String] as? NSNumber)?.boolValue
?? record[kCGWindowIsOnscreen as String] as? Bool
?? false
return MenuBarStatusItemWindowSnapshot(
name: name,
ownerName: ownerName,
bounds: bounds,
isOnscreen: isOnscreen,
displayBounds: displayBounds.first { $0.intersects(bounds) })
}
private static func bounds(_ value: Any?) -> CGRect? {
guard let dictionary = value as? [String: Any],
let x = self.double(dictionary["X"]),
let y = self.double(dictionary["Y"]),
let width = self.double(dictionary["Width"]),
let height = self.double(dictionary["Height"])
else { return nil }
return CGRect(x: x, y: y, width: width, height: height)
}
private static func double(_ value: Any?) -> Double? {
switch value {
case let number as NSNumber:
number.doubleValue
case let double as Double:
double
case let int as Int:
Double(int)
case let cgFloat as CGFloat:
Double(cgFloat)
default:
nil
}
}
}
@@ -0,0 +1,464 @@
import AppKit
import Foundation
struct StatusItemVisibilitySnapshot: Equatable {
let isVisible: Bool
let hasButton: Bool
let hasWindow: Bool
let hasScreen: Bool
let isOnCurrentScreen: Bool
let buttonWidth: CGFloat
init(
isVisible: Bool,
hasButton: Bool,
hasWindow: Bool,
hasScreen: Bool,
isOnCurrentScreen: Bool = true,
buttonWidth: CGFloat)
{
self.isVisible = isVisible
self.hasButton = hasButton
self.hasWindow = hasWindow
self.hasScreen = hasScreen
self.isOnCurrentScreen = isOnCurrentScreen
self.buttonWidth = buttonWidth
}
}
extension StatusItemVisibilitySnapshot: CustomStringConvertible {
var description: String {
"visible=\(self.isVisible),button=\(self.hasButton),window=\(self.hasWindow),"
+ "screen=\(self.hasScreen),currentScreen=\(self.isOnCurrentScreen),"
+ "width=\(String(format: "%.1f", Double(self.buttonWidth)))"
}
}
struct StatusItemStartupVisibilityEvidence: Equatable, CustomStringConvertible {
let autosaveName: String
let expectsVisibility: Bool
let visibilityDefault: Bool?
let snapshot: StatusItemVisibilitySnapshot
var description: String {
"name=\(self.autosaveName),expected=\(self.expectsVisibility),"
+ "default=\(self.visibilityDefault.map(String.init) ?? "unset"),\(self.snapshot)"
}
}
@MainActor
func isStatusItemBlocked(_ item: NSStatusItem) -> Bool {
MenuBarVisibilityWatcher.isBlockedSnapshot(snapshot: MenuBarVisibilityWatcher.visibilitySnapshot(item))
}
enum MenuBarVisibilityWatcher {
static let guidanceShownKey = "hasShownTahoeAllowListGuidance"
static let guidanceLastShownAtKey = "tahoeAllowListGuidanceLastShownAt"
static let guidanceRepeatInterval: TimeInterval = 24 * 60 * 60
static let startupFreshnessInterval: TimeInterval = 10
static let startupCheckDelay: TimeInterval = 2
static let screenChangeCheckDelay: Duration = .milliseconds(750)
static let screenChangeFollowUpDelay: Duration = .seconds(2)
static let settingsURL = URL(string: "x-apple.systempreferences:com.apple.MenuBarSettings")!
@MainActor
static func visibilitySnapshot(_ item: NSStatusItem) -> StatusItemVisibilitySnapshot {
let screen = item.button?.window?.screen
return StatusItemVisibilitySnapshot(
isVisible: item.isVisible,
hasButton: item.button != nil,
hasWindow: item.button?.window != nil,
hasScreen: screen != nil,
isOnCurrentScreen: screen.map(self.isCurrentScreen) ?? false,
buttonWidth: item.button?.frame.size.width ?? 0)
}
@MainActor
private static func isCurrentScreen(_ screen: NSScreen) -> Bool {
let screenNumber = self.screenNumber(screen)
return NSScreen.screens.contains { candidate in
if let screenNumber, let candidateNumber = self.screenNumber(candidate) {
return candidateNumber == screenNumber
}
return candidate === screen
}
}
private static func screenNumber(_ screen: NSScreen) -> NSNumber? {
screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber
}
static func isBlockedSnapshot(snapshot: StatusItemVisibilitySnapshot) -> Bool {
guard snapshot.isVisible else { return false }
guard snapshot.hasButton else { return true }
// Menu bar managers can park status-item windows off the current screen while preserving the
// underlying NSStatusItem. Recreating in that state makes those managers see a new item.
return !snapshot.hasWindow || snapshot.buttonWidth <= 0
}
static func isDisplacedSnapshot(snapshot: StatusItemVisibilitySnapshot) -> Bool {
guard snapshot.isVisible, snapshot.hasButton, snapshot.hasWindow, snapshot.buttonWidth > 0 else {
return false
}
return !snapshot.hasScreen || !snapshot.isOnCurrentScreen
}
static func hasBlockedVisibleSnapshots(_ snapshots: [StatusItemVisibilitySnapshot]) -> Bool {
let visibleItems = snapshots.filter(\.isVisible)
guard !visibleItems.isEmpty else { return false }
return visibleItems.allSatisfy { snapshot in
self.isBlockedSnapshot(snapshot: snapshot)
}
}
static func hasAnyBlockedVisibleSnapshot(_ snapshots: [StatusItemVisibilitySnapshot]) -> Bool {
snapshots.contains { snapshot in
snapshot.isVisible && self.isBlockedSnapshot(snapshot: snapshot)
}
}
static func hasAnyDisplacedVisibleSnapshot(_ snapshots: [StatusItemVisibilitySnapshot]) -> Bool {
snapshots.contains { snapshot in
self.isDisplacedSnapshot(snapshot: snapshot)
}
}
static func hasAnyStartupRecoveryCandidate(
snapshots: [StatusItemVisibilitySnapshot],
evidence: [StatusItemStartupVisibilityEvidence] = [],
windowSnapshots: [MenuBarStatusItemWindowSnapshot] = [],
detectTahoeBlockedStatusItem: Bool = false)
-> Bool
{
if self.hasAnyBlockedVisibleSnapshot(snapshots) {
return true
}
if detectTahoeBlockedStatusItem,
self.hasAnyTahoeHiddenNoProxyCandidate(evidence: evidence, windowSnapshots: windowSnapshots)
{
return true
}
guard detectTahoeBlockedStatusItem,
self.hasAnyDisplacedVisibleSnapshot(snapshots),
windowSnapshots.contains(where: \.isTahoeBlockedProxy)
else {
return false
}
return true
}
static func hasAnyTahoeHiddenNoProxyCandidate(
evidence: [StatusItemStartupVisibilityEvidence],
windowSnapshots: [MenuBarStatusItemWindowSnapshot])
-> Bool
{
evidence.contains { item in
// Tahoe can destroy the Control Center scene while leaving its enabled default behind.
// Requiring both app intent and that default avoids treating ordinary hidden items as blocked.
item.expectsVisibility
&& item.visibilityDefault == true
&& !item.snapshot.isVisible
&& !item.snapshot.hasWindow
&& !windowSnapshots.contains {
$0.name == item.autosaveName && $0.isOnscreen && $0.isWithinDisplayBounds
}
}
}
@MainActor
static func visibilitySnapshots(_ items: [NSStatusItem]) -> [StatusItemVisibilitySnapshot] {
items.map { item in
self.visibilitySnapshot(item)
}
}
@MainActor
static func hasBlockedVisibleStatusItems(_ items: [NSStatusItem]) -> Bool {
self.hasBlockedVisibleSnapshots(self.visibilitySnapshots(items))
}
static func shouldAttemptStartupRecovery(
appLaunchedAt: Date,
now: Date = Date(),
snapshots: [StatusItemVisibilitySnapshot],
evidence: [StatusItemStartupVisibilityEvidence] = [],
windowSnapshots: [MenuBarStatusItemWindowSnapshot] = [],
detectTahoeBlockedStatusItem: Bool = false)
-> Bool
{
guard now.timeIntervalSince(appLaunchedAt) <= self.startupFreshnessInterval else { return false }
return self.hasAnyStartupRecoveryCandidate(
snapshots: snapshots,
evidence: evidence,
windowSnapshots: windowSnapshots,
detectTahoeBlockedStatusItem: detectTahoeBlockedStatusItem)
}
static func shouldRefreshScreenChangePlacement(
previousScreenCount _: Int,
currentScreenCount _: Int,
snapshots: [StatusItemVisibilitySnapshot])
-> Bool
{
self.hasAnyDisplacedVisibleSnapshot(snapshots)
}
static func shouldAttemptScreenChangeRecovery(snapshots: [StatusItemVisibilitySnapshot]) -> Bool {
self.hasAnyBlockedVisibleSnapshot(snapshots)
}
static func shouldShowGuidance(defaults: UserDefaults, now: Date = Date()) -> Bool {
guard defaults.bool(forKey: self.guidanceShownKey) else { return true }
let lastShownAt = defaults.double(forKey: self.guidanceLastShownAtKey)
guard lastShownAt > 0 else { return false }
return now.timeIntervalSince1970 - lastShownAt >= self.guidanceRepeatInterval
}
static func markGuidanceShown(defaults: UserDefaults, now: Date = Date()) {
defaults.set(true, forKey: self.guidanceShownKey)
defaults.set(now.timeIntervalSince1970, forKey: self.guidanceLastShownAtKey)
}
@MainActor
static func presentGuidance(
defaults: UserDefaults,
now: Date = Date(),
openURL: (URL) -> Void = { NSWorkspace.shared.open($0) })
{
self.markGuidanceShown(defaults: defaults, now: now)
let alert = NSAlert()
alert.messageText = L("CodexBar can't show its menu bar icon")
alert.informativeText = L(
"macOS Tahoe can block menu bar apps in System Settings → Menu Bar → Allow in the Menu Bar. "
+ "CodexBar is running, but macOS may be hiding its icon. Open Menu Bar settings and turn CodexBar on.")
alert.alertStyle = .warning
alert.addButton(withTitle: L("Open Menu Bar Settings"))
alert.addButton(withTitle: L("Dismiss"))
if alert.runModal() == .alertFirstButtonReturn {
openURL(self.settingsURL)
}
}
}
extension StatusItemController {
func scheduleStartupStatusItemVisibilityCheck(appLaunchedAt: Date = Date()) {
guard !SettingsStore.isRunningTests else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + MenuBarVisibilityWatcher.startupCheckDelay) { [weak self] in
Task { @MainActor [weak self] in
self?.checkStartupStatusItemVisibility(appLaunchedAt: appLaunchedAt)
}
}
}
private func checkStartupStatusItemVisibility(appLaunchedAt: Date, now: Date = Date()) {
let evidence = self.startupStatusItemVisibilityEvidence()
let snapshots = evidence.map(\.snapshot)
let windowSnapshots = self.statusItemWindowSnapshots()
guard MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
appLaunchedAt: appLaunchedAt,
now: now,
snapshots: snapshots,
evidence: evidence,
windowSnapshots: windowSnapshots,
detectTahoeBlockedStatusItem: self.canDetectTahoeBlockedStatusItem)
else {
return
}
self.menuLogger.error(
"Status item failed to materialize or remained detached; recreating status items",
metadata: [
"snapshots": snapshots.map(\.description).joined(separator: " | "),
"evidence": evidence.map(\.description).joined(separator: " | "),
"windows": self.statusItemWindowDiagnosticsDescription(windowSnapshots),
])
self.recreateStatusItemsForVisibilityRecovery()
let recoveredEvidence = self.startupStatusItemVisibilityEvidence()
let recoveredSnapshots = recoveredEvidence.map(\.snapshot)
let recoveredWindowSnapshots = self.statusItemWindowSnapshots()
guard MenuBarVisibilityWatcher.shouldAttemptStartupRecovery(
appLaunchedAt: appLaunchedAt,
now: now,
snapshots: recoveredSnapshots,
evidence: recoveredEvidence,
windowSnapshots: recoveredWindowSnapshots,
detectTahoeBlockedStatusItem: self.canDetectTahoeBlockedStatusItem)
else {
self.menuLogger.info(
"Status item materialized after recreation",
metadata: ["snapshots": recoveredSnapshots.map(\.description).joined(separator: " | ")])
return
}
self.menuLogger.error(
"Status item still unavailable after recreation",
metadata: [
"snapshots": recoveredSnapshots.map(\.description).joined(separator: " | "),
"evidence": recoveredEvidence.map(\.description).joined(separator: " | "),
"windows": self.statusItemWindowDiagnosticsDescription(recoveredWindowSnapshots),
])
guard #available(macOS 26.0, *),
MenuBarVisibilityWatcher.shouldShowGuidance(defaults: self.settings.userDefaults, now: now)
else {
return
}
MenuBarVisibilityWatcher.presentGuidance(defaults: self.settings.userDefaults, now: now)
}
@objc func handleScreenParametersDidChange(_: Notification) {
let previousScreenCount = max(
self.pendingScreenChangePreviousCount ?? self.lastKnownScreenCount,
self.lastKnownScreenCount)
let currentScreenCount = NSScreen.screens.count
self.pendingScreenChangePreviousCount = previousScreenCount
self.lastKnownScreenCount = currentScreenCount
self.scheduleScreenChangeStatusItemVisibilityCheck(
previousScreenCount: previousScreenCount,
currentScreenCount: currentScreenCount)
}
private func scheduleScreenChangeStatusItemVisibilityCheck(
previousScreenCount: Int,
currentScreenCount: Int)
{
guard !SettingsStore.isRunningTests else { return }
self.screenChangeVisibilityTask?.cancel()
self.screenChangeVisibilityTask = Task { @MainActor [weak self] in
do {
try await Task.sleep(for: MenuBarVisibilityWatcher.screenChangeCheckDelay)
} catch {
return
}
self?.checkScreenChangeStatusItemVisibility(
previousScreenCount: previousScreenCount,
currentScreenCount: currentScreenCount)
}
}
private func checkScreenChangeStatusItemVisibility(previousScreenCount: Int, currentScreenCount: Int) {
self.pendingScreenChangePreviousCount = nil
let settledCurrentScreenCount = NSScreen.screens.count
self.lastKnownScreenCount = settledCurrentScreenCount
let snapshots = MenuBarVisibilityWatcher.visibilitySnapshots(self.startupVisibilityStatusItems)
if MenuBarVisibilityWatcher.shouldAttemptScreenChangeRecovery(snapshots: snapshots) {
self.menuLogger.error(
"Display configuration changed; recreating status items",
metadata: [
"previousScreenCount": "\(previousScreenCount)",
"currentScreenCount": "\(settledCurrentScreenCount)",
"capturedScreenCount": "\(currentScreenCount)",
"snapshots": snapshots.map(\.description).joined(separator: " | "),
"windows": self.statusItemWindowDiagnosticsDescription(),
])
self.recreateStatusItemsForVisibilityRecovery()
self.schedulePostScreenChangeRecoveryVerification(attempt: 1)
return
}
guard MenuBarVisibilityWatcher.shouldRefreshScreenChangePlacement(
previousScreenCount: previousScreenCount,
currentScreenCount: settledCurrentScreenCount,
snapshots: snapshots)
else {
return
}
self.menuLogger.info(
"Display configuration changed; refreshing existing status items",
metadata: [
"previousScreenCount": "\(previousScreenCount)",
"currentScreenCount": "\(settledCurrentScreenCount)",
"capturedScreenCount": "\(currentScreenCount)",
"snapshots": snapshots.map(\.description).joined(separator: " | "),
])
self.refreshExistingStatusItemsForVisibilityRecovery()
}
private func schedulePostScreenChangeRecoveryVerification(attempt: Int) {
self.screenChangeVisibilityTask = Task { @MainActor [weak self] in
do {
try await Task.sleep(for: MenuBarVisibilityWatcher.screenChangeFollowUpDelay)
} catch {
return
}
self?.verifyScreenChangeRecoveryIfNeeded(attempt: attempt)
}
}
private func verifyScreenChangeRecoveryIfNeeded(attempt: Int) {
let snapshots = MenuBarVisibilityWatcher.visibilitySnapshots(self.startupVisibilityStatusItems)
guard MenuBarVisibilityWatcher.hasAnyBlockedVisibleSnapshot(snapshots) else {
self.menuLogger.info(
"Status item recovered after display-change recovery",
metadata: ["attempt": "\(attempt)", "snapshots": snapshots.map(\.description).joined(separator: " | ")])
return
}
self.menuLogger.error(
"Status item still blocked after display-change recovery; recreating status items again",
metadata: [
"attempt": "\(attempt)",
"snapshots": snapshots.map(\.description).joined(separator: " | "),
"windows": self.statusItemWindowDiagnosticsDescription(),
])
self.recreateStatusItemsForVisibilityRecovery()
// No further async retries: a menu bar manager may park the newly recreated item in a state
// that still looks blocked, causing repeated NSStatusItem destruction that corrupts Control Center.
// Instead, do one synchronous re-check to surface guidance if macOS itself is blocking the item.
let finalSnapshots = MenuBarVisibilityWatcher.visibilitySnapshots(self.startupVisibilityStatusItems)
guard MenuBarVisibilityWatcher.hasAnyBlockedVisibleSnapshot(finalSnapshots) else { return }
self.menuLogger.error(
"Status item still blocked after display-change recovery recreation",
metadata: [
"snapshots": finalSnapshots.map(\.description).joined(separator: " | "),
"windows": self.statusItemWindowDiagnosticsDescription(),
])
guard #available(macOS 26.0, *),
MenuBarVisibilityWatcher.shouldShowGuidance(defaults: self.settings.userDefaults)
else { return }
MenuBarVisibilityWatcher.presentGuidance(defaults: self.settings.userDefaults)
}
private var startupVisibilityStatusItems: [NSStatusItem] {
[self.statusItem] + Array(self.statusItems.values)
}
private func startupStatusItemVisibilityEvidence() -> [StatusItemStartupVisibilityEvidence] {
self.startupVisibilityStatusItems.map { item in
let autosaveName = item.autosaveName ?? ""
return StatusItemStartupVisibilityEvidence(
autosaveName: autosaveName,
expectsVisibility: self.expectedVisibleStatusItemAutosaveNames.contains(autosaveName),
visibilityDefault: MenuBarStatusItemDefaultsRepair.visibilityDefault(
defaults: self.settings.userDefaults,
autosaveName: autosaveName),
snapshot: MenuBarVisibilityWatcher.visibilitySnapshot(item))
}
}
private var canDetectTahoeBlockedStatusItem: Bool {
if #available(macOS 26.0, *) {
return true
}
return false
}
private func statusItemWindowSnapshots() -> [MenuBarStatusItemWindowSnapshot] {
let names = Set(self.startupVisibilityStatusItems.compactMap { item in
item.autosaveName.isEmpty ? nil : item.autosaveName
})
return MenuBarStatusItemWindowProbe.snapshots(matching: names)
}
private func statusItemWindowDiagnosticsDescription(
_ snapshots: [MenuBarStatusItemWindowSnapshot]? = nil)
-> String
{
let snapshots = snapshots ?? self.statusItemWindowSnapshots()
guard !snapshots.isEmpty else { return "none" }
return snapshots.map(\.description).joined(separator: " | ")
}
}
@@ -0,0 +1,310 @@
import AppKit
import SwiftUI
/// Hosts a menu-card SwiftUI row whose selection highlight is rendered entirely by AppKit/Core
/// Animation instead of SwiftUI, so moving the highlight while scrolling costs no SwiftUI body
/// re-evaluation or content re-rasterization.
///
/// The reported Overview scroll stutter comes from driving the native selection look through SwiftUI:
/// each scroll step flips `menuItemHighlighted`, which re-renders the entire rich row subtree
/// (header, usage bars, storage line). A headless benchmark measured ~310 ms per toggle with
/// spikes past one 120 Hz frame, matching the dropped frames in the bug report.
///
/// This view keeps the SwiftUI content pinned to its normal (unselected) appearance and recreates
/// the selected look in two GPU-composited steps that never touch the SwiftUI graph:
/// 1. an `NSVisualEffectView` with the native `.selection` material drawn behind the content, and
/// 2. a `CIColorMatrix` content filter that maps the row's pixels to the selected text color
/// this matches the existing design, where every element already becomes
/// `selectedMenuItemTextColor` when highlighted.
/// Toggling selection then costs a layer property change (~0.05 ms) rather than a SwiftUI pass.
@MainActor
final class GPUSelectionHostingView<Content: View>: NSView, MenuCardHighlighting, MenuCardMeasuring {
private let hosting: NSHostingView<MenuCardSectionContainerView<Content>>
private let selectionView = NSVisualEffectView()
private var tintFilter: CIFilter?
private var isRowHighlighted = false
private var onClick: (() -> Void)?
private let containsInteractiveControls: Bool
private let interactiveRegionStore: MenuCardInteractiveRegionStore?
private(set) var allowsMenuHighlight: Bool
/// Selection inset/radius mirror the SwiftUI `MenuCardSectionContainerView` highlight
/// (`.padding(.horizontal, 6).padding(.vertical, 2)` with a 6 pt corner radius) so the AppKit
/// background lands in the same place the SwiftUI one used to.
private static var selectionHorizontalInset: CGFloat {
6
}
private static var selectionVerticalInset: CGFloat {
2
}
private static var selectionCornerRadius: CGFloat {
6
}
/// Short enough that a fast flick still looks crisp, long enough to read as a glide rather than
/// a hard cut. Tunable from real-device recordings.
private static var selectionFadeDuration: CFTimeInterval {
0.06
}
init(
rootView: MenuCardSectionContainerView<Content>,
allowsMenuHighlight: Bool,
containsInteractiveControls: Bool = false,
interactiveRegionStore: MenuCardInteractiveRegionStore? = nil,
onClick: (() -> Void)?)
{
self.hosting = NSHostingView(rootView: rootView)
self.allowsMenuHighlight = allowsMenuHighlight
self.containsInteractiveControls = containsInteractiveControls
self.interactiveRegionStore = interactiveRegionStore
self.onClick = onClick
self.tintFilter = nil
super.init(frame: .zero)
self.wantsLayer = true
self.refreshTintFilter()
self.setupSelectionView()
self.setupHosting()
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var allowsVibrancy: Bool {
true
}
override var intrinsicContentSize: NSSize {
NSSize(width: self.frame.width, height: self.hosting.intrinsicContentSize.height)
}
override func acceptsFirstMouse(for _: NSEvent?) -> Bool {
true
}
override func viewDidChangeEffectiveAppearance() {
super.viewDidChangeEffectiveAppearance()
self.refreshTintFilter()
}
/// Forward accessibility activation to the click handler, mirroring `MenuCardItemHostingView`.
override func accessibilityRole() -> NSAccessibility.Role? {
self.onClick == nil ? super.accessibilityRole() : .button
}
override func accessibilityPerformPress() -> Bool {
guard let onClick = self.onClick else {
return super.accessibilityPerformPress()
}
onClick()
return true
}
override func hitTest(_ point: NSPoint) -> NSView? {
let descendant = super.hitTest(point)
if let descendant {
var current: NSView? = descendant
while let view = current, view !== self {
if view is NSButton || view is NSControl {
return descendant
}
current = view.superview
}
if self.hitsHostedInteractiveControl(at: point) {
return descendant
}
if descendant !== self, self.onClick != nil {
return self
}
}
return descendant
}
private func hitsHostedInteractiveControl(at point: NSPoint) -> Bool {
guard self.containsInteractiveControls else { return false }
let hostedPoint = self.hosting.convert(point, from: self)
return self.interactiveRegionStore?.contains(
hostedPoint,
hostingBounds: self.hosting.bounds,
fittedSize: self.hosting.fittingSize) == true
}
private func locationInView(for event: NSEvent) -> NSPoint {
guard self.window != nil else {
return event.locationInWindow
}
return self.convert(event.locationInWindow, from: nil)
}
override func mouseDown(with event: NSEvent) {
guard event.type == .leftMouseDown, self.onClick != nil else {
super.mouseDown(with: event)
return
}
guard self.bounds.contains(self.locationInView(for: event)), let window = self.window else { return }
// A submenu-backed NSMenuItem consumes mouseUp in its nested tracking loop before a custom
// view receives it. Track the drag/up sequence directly so release-inside cancellation stays
// native while the menu never gets a chance to close before the row action runs.
var shouldInvoke = false
window.trackEvents(
matching: [.leftMouseDragged, .leftMouseUp],
timeout: NSEvent.foreverDuration,
mode: .eventTracking)
{ [weak self] trackedEvent, stop in
guard let self, let trackedEvent else {
stop.pointee = true
return
}
if self.primaryPressShouldYieldToMenu(for: trackedEvent) {
// We dequeued this drag from the window; put it back so NSMenu's tracking loop can
// continue native drag-to-submenu selection from the same event.
window.postEvent(trackedEvent, atStart: true)
stop.pointee = true
return
}
guard let decision = self.primaryPressDecision(for: trackedEvent) else { return }
shouldInvoke = decision
stop.pointee = true
}
if shouldInvoke {
self.onClick?()
}
}
private func primaryPressDecision(for event: NSEvent) -> Bool? {
guard event.type == .leftMouseUp else { return nil }
return self.bounds.contains(self.locationInView(for: event))
}
private func primaryPressShouldYieldToMenu(for event: NSEvent) -> Bool {
event.type == .leftMouseDragged && !self.bounds.contains(self.locationInView(for: event))
}
override func layout() {
super.layout()
self.selectionView.frame = self.bounds.insetBy(
dx: Self.selectionHorizontalInset,
dy: Self.selectionVerticalInset)
self.selectionView.layer?.cornerRadius = Self.selectionCornerRadius
self.hosting.frame = self.bounds
}
func setHighlighted(_ highlighted: Bool) {
guard self.isRowHighlighted != highlighted else { return }
self.isRowHighlighted = highlighted
// Tint the content to the selected text color via a GPU color matrix; clearing the
// filter returns it to its normal palette. No SwiftUI invalidation happens here.
if let tintFilter {
self.hosting.layer?.filters = highlighted ? [tintFilter] : []
}
// Crossfade the selection background instead of hard-cutting it. As the wheel moves the
// highlight, the leaving row fades out while the arriving row fades in, which reads as the
// selection gliding between rows rather than teleporting. The fade is short so fast flicks
// still resolve crisply. Runs entirely on the GPU via Core Animation.
let layer = self.selectionView.layer
let fade = CABasicAnimation(keyPath: "opacity")
fade.fromValue = layer?.presentation()?.opacity ?? (highlighted ? 0 : 1)
fade.toValue = highlighted ? 1 : 0
fade.duration = Self.selectionFadeDuration
fade.timingFunction = CAMediaTimingFunction(name: .easeOut)
layer?.add(fade, forKey: "selectionFade")
layer?.opacity = highlighted ? 1 : 0
}
func measuredHeight(width: CGFloat) -> CGFloat {
self.hosting.frame = NSRect(origin: self.hosting.frame.origin, size: NSSize(width: width, height: 1))
self.hosting.layoutSubtreeIfNeeded()
return self.hosting.fittingSize.height
}
#if DEBUG
/// True once the menu marks this row highlighted via `setHighlighted`.
var isHighlightedForTesting: Bool {
self.isRowHighlighted
}
/// The hosted SwiftUI highlight state, which must stay `false` for GPU-selected rows proving
/// selection never re-invalidates the SwiftUI graph while scrolling.
var swiftUIHighlightStateIsHighlightedForTesting: Bool {
self.hosting.rootView.highlightState.isHighlighted
}
#endif
private func setupSelectionView() {
self.selectionView.material = .selection
self.selectionView.blendingMode = .withinWindow
self.selectionView.state = .active
self.selectionView.isEmphasized = true
self.selectionView.wantsLayer = true
self.selectionView.layer?.masksToBounds = true
// Visibility is driven by layer opacity (crossfaded in `setHighlighted`) rather than
// `isHidden`, so the selection can glide in and out instead of hard-cutting.
self.selectionView.layer?.opacity = 0
self.selectionView.autoresizingMask = [.width, .height]
self.addSubview(self.selectionView)
}
private func setupHosting() {
self.hosting.wantsLayer = true
self.hosting.autoresizingMask = [.width, .height]
self.addSubview(self.hosting)
}
/// Maps every pixel's RGB to the system selected-menu-item text color while preserving alpha,
/// reproducing the appearance the SwiftUI rows already adopt when highlighted. The bias is read
/// from `NSColor.selectedMenuItemTextColor` rather than hard-coded to white so graphite/
/// high-contrast/accessibility appearances tint correctly. Core Image runs this on the GPU
/// (Metal), so it composites for free per frame.
private func refreshTintFilter() {
self.tintFilter = Self.makeSelectedTextTintFilter(appearance: self.effectiveAppearance)
if self.isRowHighlighted {
self.hosting.layer?.filters = self.tintFilter.map { [$0] } ?? []
}
}
private static func makeSelectedTextTintFilter(appearance: NSAppearance) -> CIFilter? {
guard let filter = CIFilter(name: "CIColorMatrix") else { return nil }
var tint: NSColor = .white
appearance.performAsCurrentDrawingAppearance {
tint = NSColor.selectedMenuItemTextColor.usingColorSpace(.deviceRGB) ?? .white
}
filter.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputRVector")
filter.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputGVector")
filter.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputBVector")
filter.setValue(CIVector(x: 0, y: 0, z: 0, w: 1), forKey: "inputAVector")
filter.setValue(
CIVector(x: tint.redComponent, y: tint.greenComponent, z: tint.blueComponent, w: 0),
forKey: "inputBiasVector")
return filter
}
}
#if DEBUG
extension GPUSelectionHostingView {
func _test_hitsHostedInteractiveControl(at point: NSPoint) -> Bool {
self.hitsHostedInteractiveControl(at: point)
}
func _test_simulateRuntimeClick(at point: NSPoint? = nil) -> Bool {
let clickPoint = point ?? NSPoint(x: self.bounds.midX, y: self.bounds.midY)
guard let onClick = self.onClick, self.hitTest(clickPoint) === self else { return false }
guard self.bounds.contains(clickPoint) else { return false }
onClick()
return true
}
func _test_primaryPressDecision(for event: NSEvent) -> Bool? {
self.primaryPressDecision(for: event)
}
func _test_primaryPressShouldYieldToMenu(for event: NSEvent) -> Bool {
self.primaryPressShouldYieldToMenu(for: event)
}
}
#endif
@@ -0,0 +1,177 @@
import Foundation
extension UsageMenuCardView.Model {
func heightFingerprint(section: String, additional: [String] = []) -> String {
let notesFingerprint = MenuCardHeightFingerprint.join(self.usageNotes.map {
MenuCardHeightFingerprint.field("note", $0)
})
return MenuCardHeightFingerprint.join([
"section=\(section)",
"provider=\(self.provider.rawValue)",
"localization=\(codexBarLocalizationSignature())",
MenuCardHeightFingerprint.field("name", self.providerName),
MenuCardHeightFingerprint.field("email", self.email),
MenuCardHeightFingerprint.field("subtitle", self.subtitleText),
"subtitleStyle=\(self.subtitleStyle.heightFingerprint)",
MenuCardHeightFingerprint.field("plan", self.planText),
MenuCardHeightFingerprint.field("placeholder", self.placeholder),
MenuCardHeightFingerprint.field("credits", self.creditsText),
"creditsRemaining=\(self.creditsRemaining.map(String.init(describing:)) ?? "nil")",
MenuCardHeightFingerprint.field("creditsHint", self.creditsHintText),
MenuCardHeightFingerprint.field("creditsCopy", self.creditsHintCopyText),
"codexResetCredits=\(self.codexResetCredits?.heightFingerprint ?? "")",
"metrics=\(MenuCardHeightFingerprint.join(self.metrics.map(\.heightFingerprint)))",
"notes=\(notesFingerprint)",
"dashboard=\(self.inlineUsageDashboard?.heightFingerprint ?? "")",
"providerCost=\(self.providerCost?.heightFingerprint ?? "")",
"tokenUsage=\(self.tokenUsage?.heightFingerprint ?? "")",
"openaiAPI=\(self.openAIAPIUsage == nil ? "0" : "1")",
] + additional)
}
static func heightFingerprintField(_ name: String, _ value: String?) -> String {
MenuCardHeightFingerprint.field(name, value)
}
}
private enum MenuCardHeightFingerprint {
private static let hashSalt = UUID()
static func join(_ values: [String]) -> String {
values.map { "\($0.count):\($0)" }.joined(separator: "|")
}
static func field(_ name: String, _ value: String?) -> String {
guard let value else {
return "\(name)=nil"
}
return "\(name)=\(Self.stringShape(value))"
}
private static func stringShape(_ value: String) -> String {
var hasher = Hasher()
hasher.combine(Self.hashSalt)
hasher.combine(value)
let digest = String(UInt(bitPattern: hasher.finalize()), radix: 16)
return "chars:\(value.count),utf8:\(value.utf8.count),lines:\(Self.lineCount(value)),hash:\(digest)"
}
private static func lineCount(_ value: String) -> Int {
guard !value.isEmpty else { return 0 }
return value.utf8.reduce(1) { count, byte in
byte == 10 ? count + 1 : count
}
}
}
extension UsageMenuCardView.Model.SubtitleStyle {
fileprivate var heightFingerprint: String {
switch self {
case .info: "info"
case .loading: "loading"
case .error: "error"
}
}
}
extension UsageMenuCardView.Model.Metric {
fileprivate var heightFingerprint: String {
MenuCardHeightFingerprint.join([
self.id,
MenuCardHeightFingerprint.field("title", self.title),
"percent=\(Int(self.percent.rounded()))",
"percentStyle=\(self.percentStyle.rawValue)",
MenuCardHeightFingerprint.field("status", self.statusText),
MenuCardHeightFingerprint.field("reset", self.resetText),
MenuCardHeightFingerprint.field("detail", self.detailText),
MenuCardHeightFingerprint.field("detailLeft", self.detailLeftText),
MenuCardHeightFingerprint.field("detailRight", self.detailRightText),
self.pacePercent == nil ? "pace=0" : "pace=1",
self.paceOnTop ? "paceTop=1" : "paceTop=0",
self.cardStyle ? "card=1" : "card=0",
"warningMarkers=\(self.warningMarkerPercents.count)",
"workdayMarkers=\(self.workdayMarkerPercents.count)",
])
}
}
extension UsageMenuCardView.Model.ProviderCostSection {
fileprivate var heightFingerprint: String {
MenuCardHeightFingerprint.join([
MenuCardHeightFingerprint.field("title", self.title),
MenuCardHeightFingerprint.field("spend", self.spendLine),
MenuCardHeightFingerprint.field("percentLine", self.percentLine),
MenuCardHeightFingerprint.field("personalSpend", self.personalSpendLine),
self.percentUsed == nil ? "percent=0" : "percent=1",
])
}
}
extension UsageMenuCardView.Model.TokenUsageSection {
fileprivate var heightFingerprint: String {
MenuCardHeightFingerprint.join([
MenuCardHeightFingerprint.field("session", self.sessionLine),
MenuCardHeightFingerprint.field("month", self.monthLine),
MenuCardHeightFingerprint.field("comparisons", self.comparisonLines.joined(separator: "|")),
MenuCardHeightFingerprint.field("hint", self.hintLine),
MenuCardHeightFingerprint.field("error", self.errorLine),
MenuCardHeightFingerprint.field("errorCopy", self.errorCopyText),
])
}
}
extension CodexResetCreditsPresentation {
fileprivate var heightFingerprint: String {
MenuCardHeightFingerprint.join([
MenuCardHeightFingerprint.field("text", self.text),
MenuCardHeightFingerprint.field("expirySummary", self.expirySummaryText),
])
}
}
extension InlineUsageDashboardModel {
fileprivate var heightFingerprint: String {
MenuCardHeightFingerprint.join([
MenuCardHeightFingerprint.field("accessibility", self.accessibilityLabel),
self.valueStyle.heightFingerprint,
MenuCardHeightFingerprint.join(self.kpis.map(\.heightFingerprint)),
MenuCardHeightFingerprint.join(self.points.map(\.heightFingerprint)),
MenuCardHeightFingerprint.join(self.detailLines.map { MenuCardHeightFingerprint.field("detail", $0) }),
])
}
}
extension InlineUsageDashboardModel.KPI {
fileprivate var heightFingerprint: String {
MenuCardHeightFingerprint.join([
MenuCardHeightFingerprint.field("title", self.title),
MenuCardHeightFingerprint.field("value", self.value),
self.emphasis ? "1" : "0",
])
}
}
extension InlineUsageDashboardModel.Point {
fileprivate var heightFingerprint: String {
MenuCardHeightFingerprint.join([
self.id,
MenuCardHeightFingerprint.field("label", self.label),
MenuCardHeightFingerprint.field("accessibilityValue", self.accessibilityValue),
])
}
}
extension InlineUsageDashboardModel.ValueStyle {
fileprivate var heightFingerprint: String {
switch self {
case .currencyUSD:
"currencyUSD"
case let .currency(symbol):
"currency:\(symbol)"
case .tokens:
"tokens"
case .points:
"points"
}
}
}
@@ -0,0 +1,30 @@
import CodexBarCore
extension CodexConsumerProjection.RateLane {
var quotaWarningWindow: QuotaWarningWindow {
switch self {
case .session:
.session
case .weekly:
.weekly
}
}
}
extension UsageMenuCardView.Model {
static func warningMarkerPercents(thresholds: [Int]?, showUsed: Bool) -> [Double] {
guard let thresholds, !thresholds.isEmpty else { return [] }
return QuotaWarningThresholds.active(thresholds)
.map { showUsed ? 100 - Double($0) : Double($0) }
.filter { $0 > 0 && $0 < 100 }
}
}
/// Returns boundary percentages for work day markers on a weekly progress bar.
/// Only valid when windowMinutes == 10080 (standard 7-day week).
/// nil workDays means feature is disabled.
func workDayMarkerPercents(workDays: Int?, windowMinutes: Int?) -> [Double] {
guard workDays != nil, windowMinutes == 10080 else { return [] }
guard let wd = workDays, wd >= 2, wd <= 7 else { return [] }
return (1..<wd).map { Double($0) * 100.0 / Double(wd) }
}
@@ -0,0 +1,111 @@
import CodexBarCore
import Observation
struct MenuCardLiveSubtitle {
let text: String
let style: UsageMenuCardView.Model.SubtitleStyle
}
/// Updates values in an already-hosted card without rebuilding its tracked NSMenu.
@MainActor
@Observable
final class MenuCardRefreshMonitor {
typealias ModelResolver = @MainActor (UsageProvider) -> UsageMenuCardView.Model?
typealias ProviderRefreshStateResolver = @MainActor (UsageProvider) -> Bool
private let resolveModel: ModelResolver
private let isProviderRefreshActive: ProviderRefreshStateResolver
/// Set while an all-providers refresh is running; individual cards freeze only while their
/// provider has active refresh work.
private var globalManualRefreshInFlight = false
/// Providers with an individual manual refresh in flight. Concurrent entries are allowed so
/// refreshing one provider does not stall or unfreeze another.
private var manualRefreshProviders: Set<UsageProvider> = []
private var frozenManualRefreshModels: [UsageProvider: UsageMenuCardView.Model] = [:]
/// True while any manual refresh (global or per-provider) is running.
var isManualRefreshInFlight: Bool {
self.globalManualRefreshInFlight || !self.manualRefreshProviders.isEmpty
}
init(
resolveModel: @escaping ModelResolver,
isProviderRefreshActive: @escaping ProviderRefreshStateResolver)
{
self.resolveModel = resolveModel
self.isProviderRefreshActive = isProviderRefreshActive
}
func beginManualRefresh(
frozenModels: [UsageProvider: UsageMenuCardView.Model],
provider: UsageProvider? = nil)
{
if let provider {
self.frozenManualRefreshModels[provider] = frozenModels[provider]
self.manualRefreshProviders.insert(provider)
} else {
self.frozenManualRefreshModels = frozenModels
self.globalManualRefreshInFlight = true
}
}
/// Balances a `beginManualRefresh` with the same `provider` argument (nil ends the global refresh).
func endManualRefresh(for provider: UsageProvider? = nil) {
if let provider {
self.manualRefreshProviders.remove(provider)
self.frozenManualRefreshModels[provider] = nil
} else {
self.globalManualRefreshInFlight = false
self.frozenManualRefreshModels.removeAll(keepingCapacity: true)
}
}
func resetManualRefresh() {
self.globalManualRefreshInFlight = false
self.manualRefreshProviders.removeAll(keepingCapacity: true)
self.frozenManualRefreshModels.removeAll(keepingCapacity: true)
}
func isManualRefreshInFlight(for provider: UsageProvider) -> Bool {
self.manualRefreshProviders.contains(provider) ||
(self.globalManualRefreshInFlight && self.isProviderRefreshActive(provider))
}
func model(
for provider: UsageProvider,
fallback: UsageMenuCardView.Model) -> UsageMenuCardView.Model
{
guard !self.isManualRefreshInFlight(for: provider) else {
guard let frozen = self.frozenManualRefreshModels[provider] else {
return fallback
}
if fallback.hasCompatibleTrackedLayout(with: frozen) {
return frozen
}
// A rebuilding menu may temporarily lose some metric rows, but retained rows and other sections
// must still match the frozen layout.
if fallback.hasCompatibleTrackedMetricSubset(of: frozen) {
return frozen
}
return fallback
}
guard let resolved = self.resolveModel(provider),
fallback.hasCompatibleTrackedLayout(with: resolved)
else {
return fallback
}
return resolved
}
func subtitle(
for provider: UsageProvider,
fallback: MenuCardLiveSubtitle) -> MenuCardLiveSubtitle
{
if self.isManualRefreshInFlight(for: provider) {
return MenuCardLiveSubtitle(text: "\(L("Refreshing"))", style: .loading)
}
guard let model = self.resolveModel(provider) else { return fallback }
return MenuCardLiveSubtitle(text: model.subtitleText, style: model.subtitleStyle)
}
}
@@ -0,0 +1,132 @@
import CodexBarCore
import SwiftUI
struct CodexResetCreditPresentationItem: Equatable {
let expiryText: String
let compactExpiryText: String
}
struct CodexResetCreditsPresentation: Equatable {
let text: String
let items: [CodexResetCreditPresentationItem]
var expirySummaryText: String {
let visibleItems = self.items.prefix(4).map(\.compactExpiryText)
let hiddenCount = self.items.count - visibleItems.count
let suffix = hiddenCount > 0 ? ["+\(hiddenCount)"] : []
return (visibleItems + suffix).joined(separator: " · ")
}
var helpText: String {
self.items.enumerated().map { index, item in
"\(index + 1). \(item.expiryText)"
}.joined(separator: "\n")
}
var accessibilityLabel: String {
[L("Limit Reset Credits"), self.text, self.helpText]
.filter { !$0.isEmpty }
.joined(separator: ", ")
}
static func make(
snapshot: CodexRateLimitResetCreditsSnapshot,
resetStyle: ResetTimeDisplayStyle,
now: Date) -> CodexResetCreditsPresentation?
{
let inventory = snapshot.availableInventory(at: now)
guard !inventory.credits.isEmpty else { return nil }
let items = inventory.credits.map { credit in
Self.presentationItem(for: credit, resetStyle: resetStyle, now: now)
}
return CodexResetCreditsPresentation(
text: Self.availableText(count: inventory.count),
items: items)
}
private static func availableText(count: Int) -> String {
count == 1 ? L("1 available") : String(format: L("%d available"), count)
}
private static func presentationItem(
for credit: CodexRateLimitResetCredit,
resetStyle: ResetTimeDisplayStyle,
now: Date) -> CodexResetCreditPresentationItem
{
guard let expiresAt = credit.expiresAt else {
return CodexResetCreditPresentationItem(expiryText: L("No expiry"), compactExpiryText: L("No expiry"))
}
let formattedTime = Self.formattedTime(expiresAt, resetStyle: resetStyle, now: now)
let compactExpiryText = resetStyle == .countdown && formattedTime.hasPrefix("in ")
? String(formattedTime.dropFirst(3))
: formattedTime
return CodexResetCreditPresentationItem(
expiryText: String(format: L("Expires %@"), formattedTime),
compactExpiryText: compactExpiryText)
}
private static func formattedTime(
_ expiresAt: Date,
resetStyle: ResetTimeDisplayStyle,
now: Date) -> String
{
switch resetStyle {
case .absolute:
return UsageFormatter.resetDescription(from: expiresAt, now: now)
case .countdown:
let countdown = UsageFormatter.resetCountdownDescription(from: expiresAt, now: now)
return countdown == "now" ? L("now") : countdown
}
}
}
struct CodexResetCreditsContent: View {
let presentation: CodexResetCreditsPresentation
@Environment(\.menuItemHighlighted) private var isHighlighted
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(L("Limit Reset Credits"))
.font(.body)
.fontWeight(.medium)
.lineLimit(1)
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(self.presentation.text)
.font(.footnote.weight(.semibold))
.foregroundStyle(MenuHighlightStyle.primary(self.isHighlighted))
.lineLimit(1)
.layoutPriority(1)
Spacer(minLength: 8)
HStack(alignment: .firstTextBaseline, spacing: 4) {
Image(systemName: "clock")
.font(.caption2)
Text(self.presentation.expirySummaryText)
.font(.caption)
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
.lineLimit(1)
.minimumScaleFactor(0.8)
}
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
.accessibilityHidden(true)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.help(self.presentation.helpText)
.accessibilityElement(children: .combine)
.accessibilityLabel(self.presentation.accessibilityLabel)
}
}
extension UsageMenuCardView.Model {
static func codexResetCredits(input: Input) -> CodexResetCreditsPresentation? {
guard input.provider == .codex,
let resetCredits = input.snapshot?.codexResetCredits
else {
return nil
}
return CodexResetCreditsPresentation.make(
snapshot: resetCredits,
resetStyle: input.resetTimeDisplayStyle,
now: input.now)
}
}
+393
View File
@@ -0,0 +1,393 @@
import CodexBarCore
import Foundation
extension UsageMenuCardView.Model.ProviderCostSection {
init(
title: String,
percentUsed: Double?,
spendLine: String,
percentLine: String?)
{
self.init(
title: title,
percentUsed: percentUsed,
spendLine: spendLine,
percentLine: percentLine,
personalSpendLine: nil)
}
}
extension UsageMenuCardView.Model {
static func sakanaPayAsYouGoSection(_ usage: SakanaPayAsYouGoSnapshot?) -> ProviderCostSection? {
guard let usage else { return nil }
return ProviderCostSection(
title: L("Extra usage"),
percentUsed: nil,
spendLine: "\(L("Balance")): \(usage.balanceDetail)",
percentLine: usage.periodUsageTotal.map { "\(L("Usage")): \(UsageFormatter.usdString($0))" })
}
static func isRequiredOpenCodeZenBalance(_ snapshot: UsageSnapshot?) -> Bool {
snapshot?.primary == nil &&
snapshot?.secondary == nil &&
snapshot?.providerCost?.period == "Zen balance"
}
static func tokenUsageSnapshot(input: Input) -> CostUsageTokenSnapshot? {
if usesProviderCostHistoryAsPrimaryDashboard(input.provider), input.snapshot != nil {
return primaryCostHistorySnapshot(input: input)
}
return input.tokenSnapshot
}
static func creditsLine(
metadata: ProviderMetadata,
snapshot: UsageSnapshot?,
credits: CreditsSnapshot?,
error: String?) -> String?
{
guard metadata.supportsCredits else { return nil }
if metadata.id == .codex, credits == nil, error == nil { return nil }
if metadata.id == .amp,
let ampUsage = snapshot?.ampUsage,
let ampCredits = self.ampCreditsLine(ampUsage)
{
return ampCredits
}
if let credits {
if let creditLimit = credits.codexCreditLimit {
return UsageFormatter.creditsString(from: creditLimit.remaining)
}
return UsageFormatter.creditsString(from: credits.remaining)
}
if let error, !error.isEmpty {
return error.trimmingCharacters(in: .whitespacesAndNewlines)
}
return L(metadata.creditsHint)
}
static func creditsProgressPercent(credits: CreditsSnapshot?) -> Double? {
credits?.codexCreditLimit?.remainingPercent
}
static func creditsScaleText(credits: CreditsSnapshot?) -> String? {
guard let limit = credits?.codexCreditLimit else { return nil }
return L("of %@", UsageFormatter.creditsNumberString(from: limit.limit))
}
static func codexCreditLimitDetail(credits: CreditsSnapshot?, now: Date) -> String? {
guard let limit = credits?.codexCreditLimit else { return nil }
var parts = [
L("%@ used", UsageFormatter.creditsNumberString(from: limit.used)),
]
if let resetsAt = limit.resetsAt {
parts.append(L("resets %@", UsageFormatter.resetDescription(from: resetsAt, now: now)))
}
return parts.joined(separator: " · ")
}
private static func ampCreditsLine(_ usage: AmpUsageDetails) -> String? {
var lines: [String] = []
if let individualCredits = usage.individualCredits {
lines.append(
"\(L("Individual credits")): \(UsageFormatter.currencyString(individualCredits, currencyCode: "USD"))")
}
lines.append(contentsOf: usage.workspaceBalances.map { workspace in
"\(L("Workspace")) \(workspace.name): " +
UsageFormatter.currencyString(workspace.remaining, currencyCode: "USD")
})
return lines.isEmpty ? nil : lines.joined(separator: "\n")
}
static func tokenUsageSection(
provider: UsageProvider,
enabled: Bool,
comparisonPeriodsEnabled: Bool,
snapshot: CostUsageTokenSnapshot?,
error: String?) -> TokenUsageSection?
{
guard ProviderDescriptorRegistry.descriptor(for: provider).tokenCost.supportsTokenCost else {
return nil
}
guard enabled else { return nil }
guard let snapshot else { return nil }
let sessionCost = snapshot.sessionCostUSD.map {
UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode)
} ?? ""
let sessionTokens = snapshot.sessionTokens.map { UsageFormatter.tokenCountString($0) }
let sessionLabel = if provider == .bedrock || provider == .mistral {
Self.latestBillingDayLabel(from: snapshot)
} else {
L("Today")
}
let sessionLine: String = {
if let sessionTokens {
return String(format: L("%@: %@ · %@ tokens"), sessionLabel, sessionCost, sessionTokens)
}
return "\(sessionLabel): \(sessionCost)"
}()
let monthCost = snapshot.last30DaysCostUSD.map {
UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode)
} ?? ""
let fallbackTokens = snapshot.daily.compactMap(\.totalTokens).reduce(0, +)
let monthTokensValue = snapshot.last30DaysTokens ?? (fallbackTokens > 0 ? fallbackTokens : nil)
let monthTokens = monthTokensValue.map { UsageFormatter.tokenCountString($0) }
let windowLabel = snapshot.historyLabel ?? Self.costHistoryWindowLabel(days: snapshot.historyDays)
let monthLine: String = {
if let monthTokens {
return String(format: L("%@: %@ · %@ tokens"), windowLabel, monthCost, monthTokens)
}
return "\(windowLabel): \(monthCost)"
}()
let err = (error?.isEmpty ?? true) ? nil : error
return TokenUsageSection(
sessionLine: sessionLine,
monthLine: monthLine,
comparisonLines: comparisonPeriodsEnabled
? snapshot.comparisonSummaries().map {
Self.costWindowLine(summary: $0, currencyCode: snapshot.currencyCode)
}
: [],
hintLine: Self.tokenUsageHint(provider: provider),
errorLine: err,
errorCopyText: (error?.isEmpty ?? true) ? nil : error)
}
static func costWindowLine(summary: CostUsageWindowSummary, currencyCode: String) -> String {
let label = Self.costHistoryWindowLabel(days: summary.days)
let cost = summary.totalCostUSD.map {
UsageFormatter.currencyString($0, currencyCode: currencyCode)
} ?? ""
guard let totalTokens = summary.totalTokens else { return "\(label): \(cost)" }
return String(
format: L("%@: %@ · %@ tokens"),
label,
cost,
UsageFormatter.tokenCountString(totalTokens))
}
static func tokenUsageHint(provider: UsageProvider) -> String? {
switch provider {
case .codex:
L("Estimated from local Codex logs for the selected account.")
case .claude:
UsageFormatter.costEstimateHint(provider: provider)
case .vertexai:
L("cost_estimate_hint")
case .bedrock:
L("AWS Cost Explorer billing can lag.")
case .openai:
L("Reported by OpenAI Admin API organization usage.")
case .mistral:
L("Reported by Mistral billing usage.")
default:
nil
}
}
static func costHistoryWindowLabel(days: Int) -> String {
days == 1 ? L("Today") : String(format: L("Last %d days"), days)
}
private static func latestBillingDayLabel(from snapshot: CostUsageTokenSnapshot) -> String {
guard let entry = bedrockLatestBillingDay(from: snapshot.daily),
let displayDate = bedrockDisplayDate(from: entry.date)
else { return L("Latest billing day") }
return String(format: L("Latest billing day (%@)"), displayDate)
}
private static func bedrockLatestBillingDay(from entries: [CostUsageDailyReport.Entry])
-> CostUsageDailyReport.Entry?
{
entries.compactMap { entry -> (entry: CostUsageDailyReport.Entry, dayKey: String)? in
guard let dayKey = bedrockBillingDayKey(from: entry.date) else { return nil }
return (entry, dayKey)
}
.max { lhs, rhs in
if lhs.dayKey != rhs.dayKey { return lhs.dayKey < rhs.dayKey }
let lCost = lhs.entry.costUSD ?? -1
let rCost = rhs.entry.costUSD ?? -1
if lCost != rCost { return lCost < rCost }
let lTokens = lhs.entry.totalTokens ?? -1
let rTokens = rhs.entry.totalTokens ?? -1
if lTokens != rTokens { return lTokens < rTokens }
return lhs.entry.date < rhs.entry.date
}?.entry
}
private static func bedrockDisplayDate(from text: String) -> String? {
guard let dayKey = bedrockBillingDayKey(from: text) else { return nil }
let monthStart = dayKey.index(dayKey.startIndex, offsetBy: 5)
let monthEnd = dayKey.index(monthStart, offsetBy: 2)
let dayStart = dayKey.index(dayKey.startIndex, offsetBy: 8)
guard
let month = Int(dayKey[monthStart..<monthEnd]),
let day = Int(dayKey[dayStart...]),
(1...Self.bedrockMonthAbbreviations.count).contains(month),
(1...31).contains(day)
else { return nil }
return "\(Self.bedrockMonthAbbreviations[month - 1]) \(day)"
}
private static let bedrockMonthAbbreviations = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
]
private static func bedrockBillingDayKey(from text: String) -> String? {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard trimmed.count == 10 else { return nil }
for (offset, character) in trimmed.enumerated() {
switch offset {
case 4, 7:
guard character == "-" else { return nil }
default:
guard character.isNumber else { return nil }
}
}
let monthStart = trimmed.index(trimmed.startIndex, offsetBy: 5)
let monthEnd = trimmed.index(monthStart, offsetBy: 2)
let dayStart = trimmed.index(trimmed.startIndex, offsetBy: 8)
let yearEnd = trimmed.index(trimmed.startIndex, offsetBy: 4)
guard
let year = Int(trimmed[..<yearEnd]),
let month = Int(trimmed[monthStart..<monthEnd]),
let day = Int(trimmed[dayStart...]),
(1...Self.bedrockMonthAbbreviations.count).contains(month),
(1...Self.daysInBedrockBillingMonth(month, year: year)).contains(day)
else { return nil }
return trimmed
}
private static func daysInBedrockBillingMonth(_ month: Int, year: Int) -> Int {
switch month {
case 2:
if year.isMultiple(of: 400) { return 29 }
if year.isMultiple(of: 100) { return 28 }
return year.isMultiple(of: 4) ? 29 : 28
case 4, 6, 9, 11:
return 30
default:
return 31
}
}
static func providerCostSection(
provider: UsageProvider,
cost: ProviderCostSnapshot?) -> ProviderCostSection?
{
if provider == .manus {
return nil
}
guard let cost else { return nil }
guard provider != .synthetic else { return nil }
if provider == .factory || provider == .devin, cost.period == "Extra usage balance" {
let balance = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode)
return ProviderCostSection(
title: L("Extra usage"),
percentUsed: nil,
spendLine: "\(L("Balance")): \(balance)",
percentLine: nil)
}
if provider == .opencodego, cost.period == "Zen balance" {
let balance = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode)
return ProviderCostSection(
title: L("Zen balance"),
percentUsed: nil,
spendLine: "\(L("Balance")): \(balance)",
percentLine: nil)
}
if provider == .minimax, cost.period == "MiniMax points balance" {
let balance = String(format: "%.0f", cost.used)
return ProviderCostSection(
title: L("Credits"),
percentUsed: nil,
spendLine: "\(L("Balance")): \(balance)",
percentLine: nil)
}
if provider == .openai || provider == .claude || provider == .litellm, cost.limit <= 0 {
let spend = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode)
let periodLabel = Self.localizedPeriodLabel(cost.period ?? "Last 30 days")
return ProviderCostSection(
title: L("API spend"),
percentUsed: nil,
spendLine: "\(periodLabel): \(spend)",
percentLine: nil)
}
if provider == .litellm {
return nil
}
if provider == .clawrouter, cost.limit <= 0 {
let spend = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode)
return ProviderCostSection(
title: "ClawRouter spend",
percentUsed: nil,
spendLine: "\(L("This month")): \(spend)",
percentLine: nil)
}
guard cost.limit > 0 else { return nil }
let used: String
let limit: String
let title: String
if provider == .clawrouter {
title = "Monthly budget"
used = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode)
limit = UsageFormatter.currencyString(cost.limit, currencyCode: cost.currencyCode)
} else if cost.currencyCode == "Quota" {
title = L("Quota usage")
used = String(format: "%.0f", cost.used)
limit = String(format: "%.0f", cost.limit)
} else {
title = L("Extra usage")
used = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode)
limit = UsageFormatter.currencyString(cost.limit, currencyCode: cost.currencyCode)
}
let percentUsed = Self.clamped((cost.used / cost.limit) * 100)
let periodLabel = Self.localizedPeriodLabel(cost.period ?? "This month")
// When the headline budget is a shared pool (e.g. Cursor team on-demand), show the
// account's own contribution underneath it.
let personalSpendLine: String? = cost.personalUsed.flatMap { personal in
personal > 0
? "\(L("Your spend")): \(UsageFormatter.currencyString(personal, currencyCode: cost.currencyCode))"
: nil
}
return ProviderCostSection(
title: title,
percentUsed: percentUsed,
spendLine: "\(periodLabel): \(used) / \(limit)",
percentLine: String(format: L("%.0f%% used"), min(100, max(0, percentUsed))),
personalSpendLine: personalSpendLine)
}
private static func localizedPeriodLabel(_ label: String) -> String {
let trimmed = label.trimmingCharacters(in: .whitespacesAndNewlines)
switch trimmed.lowercased() {
case "last 30 days":
return L("Last 30 days")
case "this month":
return L("This month")
case "today":
return L("Today")
default:
return L(trimmed)
}
}
static func clamped(_ value: Double) -> Double {
min(100, max(0, value))
}
}
+43
View File
@@ -0,0 +1,43 @@
import CodexBarCore
import Foundation
extension UsageMenuCardView.Model {
static func kiroUsageNotes(input: Input) -> [String] {
var notes: [String] = []
if let authMethod = input.snapshot?.loginMethod(for: .kiro)?
.trimmingCharacters(in: .whitespacesAndNewlines),
!authMethod.isEmpty
{
notes.append("\(L("Auth")): \(authMethod)")
}
if let overages = input.snapshot?.kiroUsage?.overagesStatus?
.trimmingCharacters(in: .whitespacesAndNewlines),
!overages.isEmpty
{
notes.append("\(L("Overages")): \(overages)")
}
let overagesEnabled = input.snapshot?.kiroUsage?.overagesStatus?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
.hasPrefix("enabled") == true
if overagesEnabled,
let overageCreditsUsed = input.snapshot?.kiroUsage?.overageCreditsUsed
{
notes.append(
"\(L("Overage usage")): \(UsageFormatter.kiroCreditNumber(overageCreditsUsed)) \(L("credits"))")
}
if overagesEnabled,
let estimatedOverageCostUSD = input.snapshot?.kiroUsage?.estimatedOverageCostUSD
{
notes.append("\(L("Overage cost")): \(UsageFormatter.usdString(estimatedOverageCostUSD))")
}
return notes
}
static func kiroPlan(snapshot: UsageSnapshot?) -> String? {
guard let plan = snapshot?.kiroUsage?.displayPlanName,
!plan.isEmpty
else { return nil }
return plan
}
}
+142
View File
@@ -0,0 +1,142 @@
import CodexBarCore
import Foundation
extension UsageMenuCardView.Model {
static func minimaxMetrics(services: [MiniMaxServiceUsage], input: Input) -> [Metric] {
let percentStyle: PercentStyle = .used
let displayNameCounts = Dictionary(grouping: services.map(\.displayName), by: { $0 }).mapValues(\.count)
return services.enumerated().map { index, service in
let used = service.usage
let displayPercent = min(100, max(0, service.percent))
let usageLabel = if service.isUnlimited {
nil as String?
} else {
String(
format: L("minimax_usage_amount_format"),
used.formatted(),
service.limit.formatted())
}
let localizedName = Self.localizedMiniMaxServiceName(service.displayName)
let title = if (displayNameCounts[service.displayName] ?? 0) > 1 {
"\(localizedName) · \(Self.displayWindowBadge(for: service.windowType))"
} else {
localizedName
}
return Metric(
id: "minimax-service-\(index)",
title: title,
percent: displayPercent,
percentStyle: percentStyle,
statusText: service.isUnlimited ? "∞ Unlimited" : nil,
resetText: Self.localizedMiniMaxResetDescription(service.resetDescription),
detailText: nil,
detailLeftText: usageLabel,
detailRightText: nil,
pacePercent: nil,
paceOnTop: true,
warningMarkerPercents: service.isUnlimited
? []
: Self.miniMaxWarningMarkerPercents(service: service, input: input),
workdayMarkerPercents: service.isUnlimited
? []
: Self.miniMaxWorkdayMarkerPercents(service: service, input: input),
cardStyle: false)
}
}
private static func miniMaxWarningMarkerPercents(service: MiniMaxServiceUsage, input: Input) -> [Double] {
switch self.miniMaxQuotaWarningWindow(for: service) {
case .session:
warningMarkerPercents(
thresholds: input.quotaWarningThresholds[.session],
showUsed: true)
case .weekly:
warningMarkerPercents(
thresholds: input.quotaWarningThresholds[.weekly],
showUsed: true)
}
}
private static func miniMaxWorkdayMarkerPercents(service: MiniMaxServiceUsage, input: Input) -> [Double] {
guard self.miniMaxQuotaWarningWindow(for: service) == .weekly else { return [] }
return workDayMarkerPercents(
workDays: input.workDaysPerWeek,
windowMinutes: self.miniMaxWindowMinutes(for: service.windowType))
}
private static func miniMaxQuotaWarningWindow(for service: MiniMaxServiceUsage) -> QuotaWarningWindow {
service.windowType.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "weekly" ? .weekly : .session
}
private static func miniMaxWindowMinutes(for windowType: String) -> Int? {
let normalized = windowType.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if normalized == "weekly" {
return 7 * 24 * 60
}
if normalized == "today" || normalized == "daily" {
return 24 * 60
}
if normalized == "5h" {
return 5 * 60
}
let pieces = normalized.split(separator: " ")
guard pieces.count >= 2, let value = Int(pieces[0]) else { return nil }
switch pieces[1] {
case "hour", "hours", "hr", "hrs":
return value * 60
case "minute", "minutes", "min", "mins":
return value
default:
return nil
}
}
private static func displayWindowBadge(for windowType: String) -> String {
let trimmed = windowType.trimmingCharacters(in: .whitespacesAndNewlines)
let normalized = trimmed.lowercased()
if normalized == "weekly" {
return L("Weekly")
}
if normalized == "5 hours" || normalized == "5 hour" || normalized == "5h" {
return "5h"
}
if normalized == "today" {
return L("Today")
}
if normalized == "daily" {
return L("Daily")
}
return trimmed.isEmpty ? windowType : trimmed
}
private static func localizedMiniMaxResetDescription(_ text: String) -> String {
let prefix = "Resets in "
guard text.hasPrefix(prefix) else { return text }
let rest = String(text.dropFirst(prefix.count))
return L("Resets in %@", rest)
}
private static func localizedMiniMaxServiceName(_ raw: String) -> String {
switch raw {
case "Text Generation", "text_generation":
L("minimax_service_text_generation")
case "Text to Speech", "text_to_speech":
L("minimax_service_text_to_speech")
case "Music Generation", "music_generation":
L("minimax_service_music_generation")
case "Image Generation", "image_generation":
L("minimax_service_image_generation")
case "lyrics_generation":
L("minimax_service_lyrics_generation")
case "coding-plan-vlm":
L("minimax_service_coding_plan_vlm")
case "coding-plan-search":
L("minimax_service_coding_plan_search")
default:
raw
}
}
}
@@ -0,0 +1,933 @@
import CodexBarCore
import SwiftUI
extension UsageMenuCardView.Model {
struct PaceDetail {
let leftLabel: String
let rightLabel: String?
let pacePercent: Double?
let paceOnTop: Bool
}
static func redactedMetricDetail(_ detail: String?, provider: UsageProvider, metricID: String) -> String? {
guard let detail else { return nil }
guard provider == .litellm,
metricID == "secondary",
detail.hasPrefix("Team "),
let separator = detail.range(of: ": ", options: .backwards)
else {
return PersonalInfoRedactor.redactEmails(in: detail, isEnabled: true)
}
return PersonalInfoRedactor.redactEmails(in: "Team\(detail[separator.lowerBound...])", isEnabled: true)
}
static func redactedMetrics(
_ metrics: [Metric],
provider: UsageProvider,
hidePersonalInfo: Bool) -> [Metric]
{
guard hidePersonalInfo else { return metrics }
return metrics.map { metric in
Metric(
id: metric.id,
title: PersonalInfoRedactor.redactEmails(in: metric.title, isEnabled: true) ?? metric.title,
percent: metric.percent,
percentStyle: metric.percentStyle,
statusText: PersonalInfoRedactor.redactEmails(in: metric.statusText, isEnabled: true),
resetText: PersonalInfoRedactor.redactEmails(in: metric.resetText, isEnabled: true),
detailText: Self.redactedMetricDetail(
metric.detailText,
provider: provider,
metricID: metric.id),
detailLeftText: PersonalInfoRedactor.redactEmails(in: metric.detailLeftText, isEnabled: true),
detailRightText: PersonalInfoRedactor.redactEmails(in: metric.detailRightText, isEnabled: true),
pacePercent: metric.pacePercent,
paceOnTop: metric.paceOnTop,
warningMarkerPercents: metric.warningMarkerPercents,
workdayMarkerPercents: metric.workdayMarkerPercents,
cardStyle: metric.cardStyle)
}
}
static func usageNotes(input: Input) -> [String] {
let subscriptionNotes = self.subscriptionMetadataNotes(snapshot: input.snapshot, provider: input.provider)
if input.provider == .sub2api {
return self.sub2APIUsageNotes(input.snapshot?.sub2APIUsage) + subscriptionNotes
}
if input.provider == .kiro {
return self.kiroUsageNotes(input: input) + subscriptionNotes
}
if input.provider == .kilo {
var notes = Self.kiloLoginDetails(snapshot: input.snapshot)
let resolvedSource = input.sourceLabel?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
if input.kiloAutoMode,
resolvedSource == "cli",
!notes.contains(where: { $0.caseInsensitiveCompare("Using CLI fallback") == .orderedSame })
{
notes.append(L("Using CLI fallback"))
}
return notes + subscriptionNotes
}
if input.provider == .mimo, input.snapshot != nil {
return Self.mimoUsageNotes(input: input, subscriptionNotes: subscriptionNotes)
}
if let notes = self.apiProviderUsageNotes(input: input) {
return notes + subscriptionNotes
}
if input.provider == .crossmodel, let crossModel = input.snapshot?.crossModelUsage {
return Self.crossModelSpendNotes(crossModel) + subscriptionNotes
}
guard input.provider == .openrouter,
let openRouter = input.snapshot?.openRouterUsage
else {
return subscriptionNotes
}
var notes = Self.openRouterSpendNotes(openRouter)
switch openRouter.keyQuotaStatus {
case .available:
break
case .noLimitConfigured:
notes.append(L("No limit set for the API key"))
case .unavailable:
notes.append(L("API key limit unavailable right now"))
}
return notes + subscriptionNotes
}
var isOverviewErrorOnly: Bool {
self.subtitleStyle == .error &&
self.metrics.isEmpty &&
self.usageNotes.isEmpty &&
self.openAIAPIUsage == nil &&
self.inlineUsageDashboard == nil &&
self.creditsRemaining == nil &&
self.providerCost == nil &&
self.tokenUsage == nil &&
self.placeholder == nil
}
var hasUsageContent: Bool {
!self.metrics.isEmpty ||
!self.usageNotes.isEmpty ||
self.openAIAPIUsage != nil ||
self.inlineUsageDashboard != nil ||
self.codexResetCredits != nil ||
self.placeholder != nil
}
var usesStackedDetailLayout: Bool {
!self.metrics.isEmpty ||
self.creditsText != nil ||
self.codexResetCredits != nil ||
self.providerCost != nil ||
self.tokenUsage != nil
}
func hasCompatibleTrackedLayout(with candidate: Self) -> Bool {
self.hasCompatibleTrackedLayout(with: candidate, includeMetrics: true)
}
func hasCompatibleTrackedLayoutIgnoringMetrics(with candidate: Self) -> Bool {
self.hasCompatibleTrackedLayout(with: candidate, includeMetrics: false)
}
func hasCompatibleTrackedMetricSubset(of candidate: Self) -> Bool {
guard self.metrics.count < candidate.metrics.count,
self.hasCompatibleTrackedLayoutIgnoringMetrics(with: candidate)
else {
return false
}
return self.metrics.allSatisfy { metric in
candidate.metrics.contains { Self.hasCompatibleMetricLayout(metric, $0) }
}
}
private func hasCompatibleTrackedLayout(with candidate: Self, includeMetrics: Bool) -> Bool {
guard self.provider == candidate.provider,
!includeMetrics || self.metrics.count == candidate.metrics.count,
self.usageNotes == candidate.usageNotes,
(self.openAIAPIUsage == nil) == (candidate.openAIAPIUsage == nil),
Self.hasCompatibleCreditsLayout(
currentText: self.creditsText,
currentRemaining: self.creditsRemaining,
candidateText: candidate.creditsText,
candidateRemaining: candidate.creditsRemaining),
self.creditsHintText == candidate.creditsHintText,
self.codexResetCredits == candidate.codexResetCredits,
self.placeholder == candidate.placeholder,
Self.hasCompatibleDashboardLayout(self.inlineUsageDashboard, candidate.inlineUsageDashboard),
Self.hasCompatibleProviderCostLayout(self.providerCost, candidate.providerCost),
Self.hasCompatibleTokenUsageLayout(self.tokenUsage, candidate.tokenUsage)
else {
return false
}
guard includeMetrics else { return true }
return zip(self.metrics, candidate.metrics).allSatisfy(Self.hasCompatibleMetricLayout)
}
private static func hasCompatibleMetricLayout(_ current: Metric, _ candidate: Metric) -> Bool {
current.id == candidate.id &&
current.title == candidate.title &&
current.percentStyle == candidate.percentStyle &&
(current.statusText == nil) == (candidate.statusText == nil) &&
(current.resetText == nil) == (candidate.resetText == nil) &&
(current.detailText == nil) == (candidate.detailText == nil) &&
(current.detailLeftText == nil) == (candidate.detailLeftText == nil) &&
(current.detailRightText == nil) == (candidate.detailRightText == nil) &&
current.cardStyle == candidate.cardStyle
}
private static func hasCompatibleCreditsLayout(
currentText: String?,
currentRemaining: Double?,
candidateText: String?,
candidateRemaining: Double?) -> Bool
{
switch (currentText, candidateText) {
case (nil, nil):
return true
case let (currentText?, candidateText?):
guard (currentRemaining == nil) == (candidateRemaining == nil) else { return false }
// Numeric balances render as a fixed single line beside the full-scale label.
// Multiline workspace balances retain their measured text until the menu reopens.
return currentRemaining != nil || currentText == candidateText
default:
return false
}
}
private static func hasCompatibleDashboardLayout(
_ current: InlineUsageDashboardModel?,
_ candidate: InlineUsageDashboardModel?) -> Bool
{
switch (current, candidate) {
case (nil, nil):
true
case let (current?, candidate?):
current.valueStyle == candidate.valueStyle &&
current.kpis.count == candidate.kpis.count &&
current.points.count == candidate.points.count &&
current.detailLines.count == candidate.detailLines.count &&
zip(current.kpis, candidate.kpis).allSatisfy {
$0.title == $1.title && $0.emphasis == $1.emphasis
} &&
zip(current.points, candidate.points).allSatisfy {
$0.id == $1.id && $0.label == $1.label
}
default:
false
}
}
private static func hasCompatibleProviderCostLayout(
_ current: ProviderCostSection?,
_ candidate: ProviderCostSection?) -> Bool
{
switch (current, candidate) {
case (nil, nil):
true
case let (current?, candidate?):
current.title == candidate.title &&
(current.percentUsed == nil) == (candidate.percentUsed == nil) &&
(current.percentLine == nil) == (candidate.percentLine == nil) &&
(current.personalSpendLine == nil) == (candidate.personalSpendLine == nil)
default:
false
}
}
private static func hasCompatibleTokenUsageLayout(
_ current: TokenUsageSection?,
_ candidate: TokenUsageSection?) -> Bool
{
switch (current, candidate) {
case (nil, nil):
true
case let (current?, candidate?):
current.hintLine == candidate.hintLine &&
current.errorLine == candidate.errorLine &&
current.comparisonLines.count == candidate.comparisonLines.count
default:
false
}
}
static func progressColor(for provider: UsageProvider) -> Color {
if provider == .elevenlabs {
return Color(nsColor: .labelColor)
}
let color = ProviderDescriptorRegistry.descriptor(for: provider).branding.color
return Color(red: color.red, green: color.green, blue: color.blue)
}
static func rateWindowLabels(
input: Input,
snapshot: UsageSnapshot) -> (primary: String, secondary: String, tertiary: String, showsTertiary: Bool)
{
if input.provider == .factory, snapshot.tertiary != nil {
return ("5-hour", L("Weekly"), L("Monthly"), true)
}
// Legacy request-based Cursor plans track a request quota, not the token-based "Total" pool.
let primaryLabel = if input.provider == .cursor, snapshot.cursorRequests != nil {
"Requests"
} else if input.provider == .grok {
GrokProviderDescriptor.primaryLabel(window: snapshot.primary) ?? input.metadata.sessionLabel
} else if input.provider == .doubao {
DoubaoProviderDescriptor.primaryLabel(window: snapshot.primary) ?? input.metadata.sessionLabel
} else if input.provider == .sub2api {
Sub2APIProviderDescriptor.primaryLabel(details: snapshot.sub2APIUsage) ?? input.metadata.sessionLabel
} else {
input.metadata.sessionLabel
}
return (
L(primaryLabel),
L(input.metadata.weeklyLabel),
input.metadata.opusLabel.map(L) ?? L("Sonnet"),
input.metadata.supportsOpus)
}
static func sub2APIUsageNotes(_ usage: Sub2APIUsageDetails?) -> [String] {
guard let usage else { return [] }
var notes: [String] = []
if let balance = usage.balance {
notes.append("\(L("Balance")): \(UsageFormatter.currencyString(balance, currencyCode: usage.unit))")
}
if let today = usage.today {
notes.append("\(L("Today")): \(self.sub2APITotalsText(today, unit: usage.unit))")
}
if let total = usage.total {
notes.append("\(L("Total")): \(self.sub2APITotalsText(total, unit: usage.unit))")
}
return notes
}
private static func sub2APITotalsText(_ totals: Sub2APIUsageDetails.Totals, unit: String) -> String {
"\(UsageFormatter.tokenCountString(totals.requests)) \(L("requests")) · " +
"\(UsageFormatter.tokenCountString(totals.totalTokens)) \(L("tokens")) · " +
UsageFormatter.currencyString(totals.actualCostUSD, currencyCode: unit)
}
static func resetText(
for window: RateWindow,
style: ResetTimeDisplayStyle,
now: Date) -> String?
{
UsageFormatter.resetLine(for: window, style: style, now: now)
}
static func placeholder(input: Input) -> String? {
if self.shouldShowRateLimitsUnavailablePlaceholder(input: input) {
return L("Limits not available")
}
if input.snapshot == nil, !input.isRefreshing, input.lastError == nil {
return self.hasLocalCodexTokenUsage(input) ? nil : L("No usage yet")
}
return nil
}
static func lastError(input: Input) -> String? {
guard let lastError = input.lastError?.trimmingCharacters(in: .whitespacesAndNewlines),
!lastError.isEmpty
else {
return nil
}
if self.shouldShowRateLimitsUnavailablePlaceholder(input: input, lastError: lastError) {
return nil
}
return lastError
}
static func dashboardHint(error: String?) -> String? {
guard let error, !error.isEmpty else { return nil }
return error
}
static func mimoUsageNotes(input: Input, subscriptionNotes: [String]) -> [String] {
let source = input.sourceLabel?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
guard source != "local" else { return [] }
return [
L("Balance updates in near-real time (up to 5 min lag)"),
L("Daily billing data finalizes at 07:00 UTC"),
] + subscriptionNotes
}
static func subscriptionMetadataNotes(snapshot: UsageSnapshot?, provider: UsageProvider) -> [String] {
guard let snapshot else { return [] }
if let renewsAt = snapshot.subscriptionRenewsAt {
return [String(format: L("Renews: %@"), self.subscriptionDateString(renewsAt, provider: provider))]
}
if let expiresAt = snapshot.subscriptionExpiresAt {
return [String(format: L("Plan expires: %@"), self.subscriptionDateString(expiresAt, provider: provider))]
}
return []
}
private static func subscriptionDateString(_ date: Date, provider: UsageProvider) -> String {
let formatter = DateFormatter()
formatter.locale = Locale.current
formatter.timeZone = self.subscriptionDateTimeZone(provider: provider)
formatter.setLocalizedDateFormatFromTemplate("MMM d, yyyy")
return formatter.string(from: date)
}
private static func subscriptionDateTimeZone(provider: UsageProvider) -> TimeZone {
switch provider {
case .minimax:
TimeZone(identifier: "Asia/Shanghai") ?? .current
default:
.current
}
}
static func poeBalanceDetailText(input: Input) -> String? {
guard input.provider == .poe else { return nil }
return StatusItemController.poeBalanceDisplayText(snapshot: input.snapshot)
}
private static func hasLocalCodexTokenUsage(_ input: Input) -> Bool {
input.provider == .codex &&
input.tokenCostUsageEnabled &&
self.tokenUsageSnapshot(input: input) != nil
}
private static func shouldShowRateLimitsUnavailablePlaceholder(input: Input, lastError: String? = nil) -> Bool {
let currentError = lastError ?? input.lastError
if let currentError = currentError?.trimmingCharacters(in: .whitespacesAndNewlines),
!currentError.isEmpty,
!UsageError.isNoRateLimitsFoundDescription(currentError),
!ClaudeStatusProbe.isSubscriptionQuotaUnavailableDescription(currentError)
{
return false
}
if input.limitsAvailability?.isUnavailable == true {
return true
}
return self.rateLimitsUnavailable(input: input, lastError: currentError)
}
private static func rateLimitsUnavailable(input: Input, lastError: String? = nil) -> Bool {
UsageLimitsAvailability.resolve(
provider: input.provider,
snapshot: input.snapshot,
account: input.account,
lastErrorDescription: lastError ?? input.lastError)
.isUnavailable
}
static func sessionPaceDetail(
provider: UsageProvider,
window: RateWindow,
now: Date,
showUsed: Bool) -> PaceDetail?
{
guard let detail = UsagePaceText.sessionDetail(provider: provider, window: window, now: now) else { return nil }
let expectedUsed = detail.expectedUsedPercent
let actualUsed = window.usedPercent
let expectedPercent = showUsed ? expectedUsed : (100 - expectedUsed)
let actualPercent = showUsed ? actualUsed : (100 - actualUsed)
if expectedPercent.isFinite == false || actualPercent.isFinite == false {
return nil
}
let paceOnTop = actualUsed <= expectedUsed
let pacePercent: Double? = if detail.stage == .onTrack {
nil
} else {
expectedPercent
}
return PaceDetail(
leftLabel: detail.leftLabel,
rightLabel: detail.rightLabel,
pacePercent: pacePercent,
paceOnTop: paceOnTop)
}
static func weeklyPaceDetail(
provider: UsageProvider,
window: RateWindow,
now: Date,
pace: UsagePace?,
showUsed: Bool) -> PaceDetail?
{
guard let pace else { return nil }
let detail = UsagePaceText.weeklyDetail(provider: provider, pace: pace, now: now)
let expectedUsed = detail.expectedUsedPercent
let actualUsed = window.usedPercent
let expectedPercent = showUsed ? expectedUsed : (100 - expectedUsed)
let actualPercent = showUsed ? actualUsed : (100 - actualUsed)
if expectedPercent.isFinite == false || actualPercent.isFinite == false {
return nil
}
let paceOnTop = actualUsed <= expectedUsed
let pacePercent: Double? = if detail.stage == .onTrack {
nil
} else {
expectedPercent
}
return PaceDetail(
leftLabel: detail.leftLabel,
rightLabel: detail.rightLabel,
pacePercent: pacePercent,
paceOnTop: paceOnTop)
}
static func standardWeeklyPace(input: Input, window: RateWindow) -> UsagePace? {
if let weeklyPace = input.weeklyPace {
return weeklyPace
}
return Self.displayableWeeklyPace(UsagePace.weekly(
window: window,
now: input.now,
defaultWindowMinutes: 10080,
workDays: input.workDaysPerWeek))
}
private static func displayableWeeklyPace(_ pace: UsagePace?) -> UsagePace? {
guard let pace else { return nil }
return pace.expectedUsedPercent >= 3 || pace.etaSeconds == 0 ? pace : nil
}
static func resetWindowPaceDetail(
window: RateWindow,
input: Input,
pace: UsagePace? = nil) -> PaceDetail?
{
guard self.supportsResetWindowPace(provider: input.provider, window: window),
window.remainingPercent > 0
else { return nil }
let paceWindow = Self.resetWindowForPace(provider: input.provider, window: window)
let resolved = pace ?? UsagePace.weekly(
window: paceWindow,
now: input.now,
defaultWindowMinutes: 10080,
workDays: input.workDaysPerWeek)
guard let resolved = Self.displayableWeeklyPace(resolved) else { return nil }
return Self.weeklyPaceDetail(
provider: input.provider,
window: paceWindow,
now: input.now,
pace: resolved,
showUsed: input.usageBarsShowUsed)
}
private static let monthlyWindowSentinelMinutes = 30 * 24 * 60
private static func supportsResetWindowPace(provider: UsageProvider, window: RateWindow) -> Bool {
switch provider {
case .cursor:
window.windowMinutes != nil
case .alibaba, .alibabatokenplan, .doubao, .opencodego:
window.windowMinutes == self.monthlyWindowSentinelMinutes
default:
false
}
}
private static func resetWindowForPace(provider: UsageProvider, window: RateWindow) -> RateWindow {
// Provider snapshots use 30 days as a monthly sentinel; use the reset date for the real calendar-cycle length.
guard self.usesInferredMonthlyDuration(provider: provider, window: window),
let resetsAt = window.resetsAt,
let minutes = self.inferredMonthlyWindowMinutes(endingAt: resetsAt)
else { return window }
return RateWindow(
usedPercent: window.usedPercent,
windowMinutes: minutes,
resetsAt: window.resetsAt,
resetDescription: window.resetDescription,
nextRegenPercent: window.nextRegenPercent,
isSyntheticPlaceholder: window.isSyntheticPlaceholder)
}
private static func usesInferredMonthlyDuration(provider: UsageProvider, window: RateWindow) -> Bool {
guard window.windowMinutes == self.monthlyWindowSentinelMinutes else { return false }
switch provider {
case .alibaba, .alibabatokenplan, .doubao, .opencodego:
return true
default:
return false
}
}
private static func inferredMonthlyWindowMinutes(endingAt resetsAt: Date) -> Int? {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(secondsFromGMT: 0) ?? calendar.timeZone
guard let startsAt = calendar.date(byAdding: .month, value: -1, to: resetsAt) else { return nil }
let minutes = resetsAt.timeIntervalSince(startsAt) / 60
guard minutes.isFinite, minutes > 0 else { return nil }
return Int(minutes.rounded())
}
static func antigravityMetrics(input: Input, snapshot: UsageSnapshot) -> [Metric] {
let percentStyle: PercentStyle = input.usageBarsShowUsed ? .used : .left
if Self.hasAntigravityQuotaSummaryWindows(snapshot) {
return Self.extraRateWindowMetrics(
snapshot: snapshot,
input: input,
percentStyle: percentStyle)
}
var metrics: [Metric] = []
if let primary = snapshot.primary {
metrics.append(Self.antigravityMetric(
id: "primary",
title: L(input.metadata.sessionLabel),
window: primary,
input: input,
percentStyle: percentStyle))
}
if let secondary = snapshot.secondary {
metrics.append(Self.antigravityMetric(
id: "secondary",
title: L(input.metadata.weeklyLabel),
window: secondary,
input: input,
percentStyle: percentStyle))
}
if input.metadata.supportsOpus, let tertiary = snapshot.tertiary {
metrics.append(Self.antigravityMetric(
id: "tertiary",
title: input.metadata.opusLabel.map(L) ?? L("Gemini Flash"),
window: tertiary,
input: input,
percentStyle: percentStyle))
}
metrics.append(contentsOf: Self.extraRateWindowMetrics(
snapshot: snapshot,
input: input,
percentStyle: percentStyle))
return metrics
}
static func extraRateWindowMetrics(
snapshot: UsageSnapshot,
input: Input,
percentStyle: PercentStyle) -> [Metric]
{
guard let extraRateWindows = snapshot.extraRateWindows else { return [] }
// Codex additional limits (e.g. Codex Spark) are optional extra usage and follow the
// "optional credits and extra usage" setting. Other providers' extra windows (Antigravity
// per-model quotas, Factory core windows, etc.) are core data and must always render.
if input.provider == .codex, !input.showOptionalCreditsAndExtraUsage {
return []
}
if input.provider == .copilot, !input.copilotBudgetExtrasEnabled {
return []
}
let visibleRateWindows = if input.provider == .codex, !input.codexSparkUsageVisible {
extraRateWindows.filter { !Self.isCodexSparkRateWindow($0) }
} else {
extraRateWindows
}
return visibleRateWindows.map { namedWindow in
let paceDetail = Self.extraRateWindowPaceDetail(
provider: input.provider,
window: namedWindow.window,
input: input)
let usageKnown = namedWindow.usageKnown
let resolvedResetText = Self.extraRateWindowResetText(
namedWindow: namedWindow,
input: input)
let resetText = input.provider == .sub2api && namedWindow.window.resetsAt == nil
? nil
: resolvedResetText
let detailText = input.provider == .sub2api
? namedWindow.window.resetDescription
: nil
let statusText: String? = if usageKnown {
nil
} else if let resetText {
"\(L("Unavailable")) - \(resetText)"
} else {
L("Unavailable")
}
return Metric(
id: namedWindow.id,
title: namedWindow.title,
percent: Self.clamped(
input.usageBarsShowUsed
? namedWindow.window.usedPercent
: namedWindow.window.remainingPercent),
percentStyle: percentStyle,
statusText: statusText,
resetText: usageKnown ? resetText : nil,
detailText: usageKnown ? detailText : nil,
detailLeftText: usageKnown ? paceDetail?.leftLabel : nil,
detailRightText: usageKnown ? paceDetail?.rightLabel : nil,
pacePercent: usageKnown ? paceDetail?.pacePercent : nil,
paceOnTop: paceDetail?.paceOnTop ?? true)
}
}
private static func isCodexSparkRateWindow(_ namedWindow: NamedRateWindow) -> Bool {
namedWindow.id == CodexAdditionalRateLimitMapper.sparkWindowID ||
namedWindow.id == CodexAdditionalRateLimitMapper.sparkWeeklyWindowID
}
private static let antigravityQuotaSummaryWindowIDPrefix = "antigravity-quota-summary-"
private static func hasAntigravityQuotaSummaryWindows(_ snapshot: UsageSnapshot) -> Bool {
snapshot.extraRateWindows?.contains(where: self.isAntigravityQuotaSummaryWindow) == true
}
private static func isAntigravityQuotaSummaryWindow(_ namedWindow: NamedRateWindow) -> Bool {
namedWindow.id.hasPrefix(self.antigravityQuotaSummaryWindowIDPrefix)
}
private static func extraRateWindowResetText(
namedWindow: NamedRateWindow,
input: Input) -> String?
{
if namedWindow.window.resetsAt != nil {
return self.resetText(
for: namedWindow.window,
style: input.resetTimeDisplayStyle,
now: input.now)
}
if input.provider == .antigravity,
self.isAntigravityQuotaSummaryWindow(namedWindow)
{
return self.antigravityQuotaSummaryResetText(namedWindow.window.resetDescription)
}
return self.resetText(
for: namedWindow.window,
style: input.resetTimeDisplayStyle,
now: input.now)
}
private static func antigravityQuotaSummaryResetText(_ description: String?) -> String? {
guard let description = description?.trimmingCharacters(in: .whitespacesAndNewlines),
!description.isEmpty
else { return nil }
if let range = description.range(of: "fully refresh in ", options: .caseInsensitive) {
var suffix = String(description[range.upperBound...])
.trimmingCharacters(in: .whitespacesAndNewlines)
while suffix.last == "." {
suffix.removeLast()
}
guard !suffix.isEmpty else { return description }
return String(format: L("Resets in %@"), suffix)
}
return description
}
private static func extraRateWindowPaceDetail(
provider: UsageProvider,
window: RateWindow,
input: Input) -> PaceDetail?
{
guard provider == .codex || provider == .antigravity else { return nil }
switch window.windowMinutes {
case 300:
return self.sessionPaceDetail(
provider: provider,
window: window,
now: input.now,
showUsed: input.usageBarsShowUsed)
case 10080:
let pace = Self.displayableWeeklyPace(UsagePace.weekly(
window: window,
now: input.now,
defaultWindowMinutes: 10080,
workDays: input.workDaysPerWeek))
return Self.weeklyPaceDetail(
provider: provider,
window: window,
now: input.now,
pace: pace,
showUsed: input.usageBarsShowUsed)
default:
return nil
}
}
private static func antigravityMetricPaceDetail(
window: RateWindow,
input: Input) -> PaceDetail?
{
guard input.provider == .antigravity else { return nil }
switch window.windowMinutes {
case nil, 300:
return self.sessionPaceDetail(
provider: input.provider,
window: window,
now: input.now,
showUsed: input.usageBarsShowUsed)
case 10080:
let pace = Self.displayableWeeklyPace(UsagePace.weekly(
window: window,
now: input.now,
defaultWindowMinutes: 10080,
workDays: input.workDaysPerWeek))
return Self.weeklyPaceDetail(
provider: input.provider,
window: window,
now: input.now,
pace: pace,
showUsed: input.usageBarsShowUsed)
default:
return nil
}
}
static func antigravityMetric(
id: String,
title: String,
window: RateWindow?,
input: Input,
percentStyle: PercentStyle) -> Metric
{
guard let window else {
let placeholderPercent = input.usageBarsShowUsed ? 100.0 : 0.0
return Metric(
id: id,
title: title,
percent: placeholderPercent,
percentStyle: percentStyle,
statusText: nil,
resetText: nil,
detailText: nil,
detailLeftText: nil,
detailRightText: nil,
pacePercent: nil,
paceOnTop: true)
}
let percent = input.usageBarsShowUsed ? window.usedPercent : window.remainingPercent
let paceDetail = Self.antigravityMetricPaceDetail(window: window, input: input)
return Metric(
id: id,
title: title,
percent: Self.clamped(percent),
percentStyle: percentStyle,
resetText: Self.resetText(for: window, style: input.resetTimeDisplayStyle, now: input.now),
detailText: nil,
detailLeftText: paceDetail?.leftLabel,
detailRightText: paceDetail?.rightLabel,
pacePercent: paceDetail?.pacePercent,
paceOnTop: paceDetail?.paceOnTop ?? true)
}
static func zaiLimitDetailText(limit: ZaiLimitEntry?) -> String? {
guard let limit else { return nil }
if let currentValue = limit.currentValue,
let usage = limit.usage,
let remaining = limit.remaining
{
let currentStr = UsageFormatter.tokenCountString(currentValue)
let usageStr = UsageFormatter.tokenCountString(usage)
let remainingStr = UsageFormatter.tokenCountString(remaining)
return String(format: L("%@ / %@ (%@ remaining)"), currentStr, usageStr, remainingStr)
}
return nil
}
static func crossModelSpendNotes(_ usage: CrossModelUsageSnapshot) -> [String] {
let candidates: [(String, Double?)] = [
(L("Today"), usage.daily?.cost),
(L("This week"), usage.weekly?.cost),
(L("This month"), usage.monthly?.cost),
]
let rendered = candidates.compactMap { candidate -> String? in
guard let value = candidate.1 else { return nil }
return "\(candidate.0): \(usage.currencyString(value))"
}
guard !rendered.isEmpty else { return [] }
return [rendered.joined(separator: " · ")]
}
static func openRouterQuotaDetail(provider: UsageProvider, snapshot: UsageSnapshot) -> String? {
guard provider == .openrouter,
let usage = snapshot.openRouterUsage,
usage.hasValidKeyQuota,
let keyRemaining = usage.keyRemaining,
let keyLimit = usage.keyLimit
else {
return nil
}
let remaining = UsageFormatter.usdString(keyRemaining)
let limit = UsageFormatter.usdString(keyLimit)
return String(format: L("%@/%@ left"), remaining, limit)
}
static func syntheticRegenDetail(
weekly: RateWindow,
cost: ProviderCostSnapshot?,
now: Date,
showUsed: Bool) -> (resetText: String, pace: PaceDetail)?
{
guard let cost,
cost.limit > 0,
let nextRegenAmount = cost.nextRegenAmount,
nextRegenAmount > 0,
let resetsAt = weekly.resetsAt
else { return nil }
let countdown = UsageFormatter.resetCountdownDescription(from: resetsAt, now: now)
let resetText = String(format: L("Regenerates %@"), countdown)
let nextRegenPercent = (nextRegenAmount / cost.limit) * 100
let afterNextRegenRemaining = min(100, weekly.remainingPercent + nextRegenPercent)
let afterNextRegen = showUsed ? max(0, 100 - afterNextRegenRemaining) : afterNextRegenRemaining
let suffix = showUsed ? L("used after next regen") : L("after next regen")
let ticksToFull = max(0, cost.used) / nextRegenAmount
let left = String(format: "%.0f%% %@", afterNextRegen, suffix)
let right = if ticksToFull <= 0.1 {
L("Near full")
} else if ticksToFull < 1.5 {
L("Full in ~1 regen")
} else {
String(format: L("Full in ~%.0f regens"), ceil(ticksToFull))
}
return (resetText, PaceDetail(leftLabel: left, rightLabel: right, pacePercent: nil, paceOnTop: true))
}
static func syntheticRollingRegenDetail(
window: RateWindow,
now: Date,
showUsed: Bool) -> (resetText: String, pace: PaceDetail)?
{
guard let resetsAt = window.resetsAt,
let nextRegenPercent = window.nextRegenPercent,
nextRegenPercent > 0
else { return nil }
let countdown = UsageFormatter.resetCountdownDescription(from: resetsAt, now: now)
let resetText = String(format: L("Regenerates %@"), countdown)
let afterNextRegenRemaining = min(100, window.remainingPercent + nextRegenPercent)
let afterNextRegen = showUsed ? max(0, 100 - afterNextRegenRemaining) : afterNextRegenRemaining
let suffix = showUsed ? L("used after next regen") : L("after next regen")
let left = String(format: "%.0f%% %@", afterNextRegen, suffix)
let missingPercent = max(0, window.usedPercent)
let ticksToFull = missingPercent / nextRegenPercent
let right = if ticksToFull <= 0.1 {
L("Near full")
} else if ticksToFull < 1.5 {
L("Full in ~1 regen")
} else {
String(format: L("Full in ~%.0f regens"), ceil(ticksToFull))
}
return (resetText, PaceDetail(leftLabel: left, rightLabel: right, pacePercent: nil, paceOnTop: true))
}
}
@@ -0,0 +1,110 @@
import CodexBarCore
import Foundation
extension UsageMenuCardView.Model {
struct Input {
let provider: UsageProvider
let metadata: ProviderMetadata
let snapshot: UsageSnapshot?
let codexProjection: CodexConsumerProjection?
let credits: CreditsSnapshot?
let creditsError: String?
let dashboard: OpenAIDashboardSnapshot?
let dashboardError: String?
let tokenSnapshot: CostUsageTokenSnapshot?
let tokenError: String?
let account: AccountInfo
let accountIsAuthoritative: Bool
let planOverride: String?
let isRefreshing: Bool
let lastError: String?
let limitsAvailability: UsageLimitsAvailability?
let usageBarsShowUsed: Bool
let resetTimeDisplayStyle: ResetTimeDisplayStyle
let tokenCostUsageEnabled: Bool
let tokenCostInlineDashboardEnabled: Bool
let tokenCostMenuSectionEnabled: Bool
let costComparisonPeriodsEnabled: Bool
let showOptionalCreditsAndExtraUsage: Bool
let codexSparkUsageVisible: Bool
let copilotBudgetExtrasEnabled: Bool
let sourceLabel: String?
let kiloAutoMode: Bool
let hidePersonalInfo: Bool
let weeklyPace: UsagePace?
let quotaWarningThresholds: [QuotaWarningWindow: [Int]]
let workDaysPerWeek: Int?
let usesLiveSubtitle: Bool
let now: Date
init(
provider: UsageProvider,
metadata: ProviderMetadata,
snapshot: UsageSnapshot?,
codexProjection: CodexConsumerProjection? = nil,
credits: CreditsSnapshot?,
creditsError: String?,
dashboard: OpenAIDashboardSnapshot?,
dashboardError: String?,
tokenSnapshot: CostUsageTokenSnapshot?,
tokenError: String?,
account: AccountInfo,
accountIsAuthoritative: Bool = false,
planOverride: String? = nil,
isRefreshing: Bool,
lastError: String?,
limitsAvailability: UsageLimitsAvailability? = nil,
usageBarsShowUsed: Bool,
resetTimeDisplayStyle: ResetTimeDisplayStyle,
tokenCostUsageEnabled: Bool,
tokenCostInlineDashboardEnabled: Bool? = nil,
tokenCostMenuSectionEnabled: Bool? = nil,
costComparisonPeriodsEnabled: Bool = false,
showOptionalCreditsAndExtraUsage: Bool,
codexSparkUsageVisible: Bool = true,
copilotBudgetExtrasEnabled: Bool = false,
sourceLabel: String? = nil,
kiloAutoMode: Bool = false,
hidePersonalInfo: Bool,
weeklyPace: UsagePace? = nil,
quotaWarningThresholds: [QuotaWarningWindow: [Int]] = [:],
workDaysPerWeek: Int? = nil,
usesLiveSubtitle: Bool = false,
now: Date)
{
self.provider = provider
self.metadata = metadata
self.snapshot = snapshot
self.codexProjection = codexProjection
self.credits = credits
self.creditsError = creditsError
self.dashboard = dashboard
self.dashboardError = dashboardError
self.tokenSnapshot = tokenSnapshot
self.tokenError = tokenError
self.account = account
self.accountIsAuthoritative = accountIsAuthoritative
self.planOverride = planOverride
self.isRefreshing = isRefreshing
self.lastError = lastError
self.limitsAvailability = limitsAvailability
self.usageBarsShowUsed = usageBarsShowUsed
self.resetTimeDisplayStyle = resetTimeDisplayStyle
self.tokenCostUsageEnabled = tokenCostUsageEnabled
self.tokenCostInlineDashboardEnabled = tokenCostInlineDashboardEnabled ?? tokenCostUsageEnabled
self.tokenCostMenuSectionEnabled = tokenCostMenuSectionEnabled ?? tokenCostUsageEnabled
self.costComparisonPeriodsEnabled = costComparisonPeriodsEnabled
self.showOptionalCreditsAndExtraUsage = showOptionalCreditsAndExtraUsage
self.codexSparkUsageVisible = codexSparkUsageVisible
self.copilotBudgetExtrasEnabled = copilotBudgetExtrasEnabled
self.sourceLabel = sourceLabel
self.kiloAutoMode = kiloAutoMode
self.hidePersonalInfo = hidePersonalInfo
self.weeklyPace = weeklyPace
self.quotaWarningThresholds = quotaWarningThresholds
self.workDaysPerWeek = workDaysPerWeek
self.usesLiveSubtitle = usesLiveSubtitle
self.now = now
}
}
}
File diff suppressed because it is too large Load Diff
+267
View File
@@ -0,0 +1,267 @@
import AppKit
import CodexBarCore
import SwiftUI
@MainActor
struct MenuContent: View {
@Bindable var store: UsageStore
@Bindable var settings: SettingsStore
let account: AccountInfo
let updater: UpdaterProviding
let provider: UsageProvider?
let actions: MenuActions
var body: some View {
let descriptor = MenuDescriptor.build(
provider: self.provider,
store: self.store,
settings: self.settings,
account: self.account,
updateReady: self.updater.updateStatus.isUpdateReady)
VStack(alignment: .leading, spacing: 8) {
ForEach(Array(descriptor.sections.enumerated()), id: \.offset) { index, section in
VStack(alignment: .leading, spacing: 4) {
ForEach(Array(section.entries.enumerated()), id: \.offset) { _, entry in
self.row(for: entry)
}
}
if index < descriptor.sections.count - 1 {
Divider()
}
}
}
.padding(.horizontal, 10)
.padding(.vertical, 6)
.frame(minWidth: 260, alignment: .leading)
}
@ViewBuilder
private func row(for entry: MenuDescriptor.Entry) -> some View {
switch entry {
case let .text(text, style):
switch style {
case .headline:
Text(text).font(.headline)
.accessibilityLabel(text)
case .primary:
Text(text)
.accessibilityLabel(text)
case .secondary:
Text(text).foregroundStyle(.secondary).font(.footnote)
.accessibilityLabel(text)
}
case let .action(title, action):
Button {
self.perform(action)
} label: {
if let icon = self.iconName(for: action) {
HStack(spacing: 8) {
Image(systemName: icon)
.imageScale(.medium)
.frame(width: 18, alignment: .center)
Text(title)
}
.foregroundStyle(.primary)
.accessibilityElement(children: .combine)
.accessibilityLabel(title)
} else {
Text(title)
.accessibilityLabel(title)
}
}
.buttonStyle(.plain)
case let .unavailable(title, tooltip):
Text(title)
.foregroundStyle(.secondary)
.help(tooltip ?? "")
case let .submenu(title, systemImageName, submenuItems):
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 8) {
if let systemImageName {
Image(systemName: systemImageName)
}
Text(title).font(.headline)
}
.accessibilityElement(children: .combine)
.accessibilityLabel(title)
ForEach(Array(submenuItems.enumerated()), id: \.offset) { _, submenuItem in
HStack(spacing: 8) {
if submenuItem.isChecked {
Image(systemName: "checkmark")
.imageScale(.small)
.frame(width: 18, alignment: .center)
} else {
Spacer().frame(width: 18)
}
Text(submenuItem.title)
.foregroundStyle(submenuItem.isEnabled ? .primary : .secondary)
}
.accessibilityElement(children: .combine)
.accessibilityLabel(submenuItem.title)
}
}
case .divider:
Divider()
}
}
private func iconName(for action: MenuDescriptor.MenuAction) -> String? {
action.systemImageName
}
private func perform(_ action: MenuDescriptor.MenuAction) {
switch action {
case .refresh:
self.actions.refresh()
case .refreshAugmentSession:
self.actions.refreshAugmentSession()
case .installUpdate:
self.actions.installUpdate()
case .dashboard:
self.actions.openDashboard()
case .statusPage:
self.actions.openStatusPage()
case .changelog:
self.actions.openChangelog()
case .addCodexAccount:
self.actions.addCodexAccount()
case .requestCodexSystemPromotion:
return
case let .addProviderAccount(provider):
self.actions.switchAccount(provider)
case let .switchAccount(provider):
self.actions.switchAccount(provider)
case let .openTerminal(command):
self.actions.openTerminal(command)
case let .loginToProvider(url):
if let urlObj = URL(string: url) {
NSWorkspace.shared.open(urlObj)
}
case .settings:
self.actions.openSettings()
case .about:
self.actions.openAbout()
case .quit:
self.actions.quit()
case let .copyError(message):
self.actions.copyError(message)
case .focusAgentSession:
return
}
}
}
struct MenuActions {
let installUpdate: () -> Void
let refresh: () -> Void
let refreshAugmentSession: () -> Void
let openDashboard: () -> Void
let openStatusPage: () -> Void
let openChangelog: () -> Void
let addCodexAccount: () -> Void
let switchAccount: (UsageProvider) -> Void
let openTerminal: (String) -> Void
let openSettings: () -> Void
let openAbout: () -> Void
let quit: () -> Void
let copyError: (String) -> Void
}
struct PersistentRefreshRowMetrics: Equatable {
static let defaults = Self(
rowHeight: 24,
selectionHorizontalInset: 5,
selectionVerticalInset: 0,
selectionCornerRadius: 7,
// Align the custom row's image/title frames with native NSMenuItem columns.
leadingPadding: 15,
trailingPadding: 8,
iconWidth: 16,
iconSymbolPointSize: 16,
iconSymbolWeight: .regular,
iconTitleSpacing: 4.5,
shortcutFontSize: 13,
shortcutXOffset: -9.5,
shortcutYOffset: 0)
let rowHeight: CGFloat
let selectionHorizontalInset: CGFloat
let selectionVerticalInset: CGFloat
let selectionCornerRadius: CGFloat
let leadingPadding: CGFloat
let trailingPadding: CGFloat
let iconWidth: CGFloat
let iconSymbolPointSize: CGFloat
let iconSymbolWeight: NSFont.Weight
let iconTitleSpacing: CGFloat
let shortcutFontSize: CGFloat
let shortcutXOffset: CGFloat
let shortcutYOffset: CGFloat
}
@MainActor
struct StatusIconView: View {
@Bindable var store: UsageStore
let provider: UsageProvider
var body: some View {
Image(nsImage: self.icon)
.renderingMode(.template)
.interpolation(.none)
.accessibilityLabel(self.accessibilityLabel)
.accessibilityValue(self.accessibilityValue)
}
private var accessibilityLabel: String {
let descriptor = ProviderDescriptorRegistry.descriptor(for: self.provider)
return descriptor.metadata.displayName
}
private var accessibilityValue: String {
let snapshot = self.store.snapshot(for: self.provider)
guard let snap = snapshot else {
return L("No data")
}
let remaining = IconRemainingResolver.resolvedRemaining(
snapshot: snap,
style: self.store.style(for: self.provider))
let primary = remaining.primary
let percent = primary.map(Self.accessibilityPercentRemaining) ?? L("Unknown")
let stale = self.store.isStale(provider: self.provider)
return stale ? "\(percent), \(L("stale data"))" : percent
}
static func accessibilityPercentRemaining(_ remaining: Double) -> String {
String(format: L("%d percent remaining"), Int(remaining.rounded()))
}
private var icon: NSImage {
let now = Date()
let snapshot = self.store.snapshot(for: self.provider)
let remaining = snapshot.map {
IconRemainingResolver.resolvedRemaining(
snapshot: $0,
style: self.store.style(for: self.provider),
now: now)
}
let creditsProjection = self.store.codexConsumerProjectionIfNeeded(
for: self.provider,
surface: .menuBar,
snapshotOverride: snapshot,
now: now)
let creditsRemaining = creditsProjection?.menuBarFallback == .creditsBalance
? self.store.codexMenuBarCreditsRemaining(
snapshotOverride: snapshot,
now: now)
: nil
return IconRenderer.makeIcon(
primaryRemaining: remaining?.primary,
weeklyRemaining: remaining?.secondary,
creditsRemaining: creditsRemaining,
stale: self.store.isStale(provider: self.provider),
style: self.store.style(for: self.provider),
statusIndicator: self.store.statusIndicator(for: self.provider),
hideCritters: self.store.settings.menuBarHidesCritters)
}
}
@@ -0,0 +1,169 @@
import CodexBarCore
import Foundation
extension MenuDescriptor {
static func appendOpenAIAPIUsageSummary(
entries: inout [Entry],
usage: OpenAIAPIUsageSnapshot)
{
let today = usage.currentDay
let last7 = usage.last7Days
let last30 = usage.last30Days
let historyLabel = usage.historyWindowLabel
entries.append(.text(
"\(L("Today")): \(UsageFormatter.usdString(today.costUSD)) · " +
"\(UsageFormatter.tokenCountString(today.totalTokens)) \(L("tokens"))",
.secondary))
entries.append(.text(
"7d: \(UsageFormatter.usdString(last7.costUSD)) · " +
"\(UsageFormatter.tokenCountString(last7.requests)) \(L("requests"))",
.secondary))
entries.append(.text(
"\(historyLabel): \(UsageFormatter.usdString(last30.costUSD)) · " +
"\(UsageFormatter.tokenCountString(last30.requests)) \(L("requests"))",
.secondary))
if let topModel = usage.topModels.first?.name {
entries.append(.text("\(L("Top model")): \(topModel)", .secondary))
}
}
static func appendClaudeAdminAPIUsageSummary(
entries: inout [Entry],
usage: ClaudeAdminAPIUsageSnapshot)
{
let today = usage.currentDay
let last7 = usage.last7Days
let last30 = usage.last30Days
entries.append(.text(
"\(L("Today")): \(UsageFormatter.usdString(today.costUSD)) · " +
"\(UsageFormatter.tokenCountString(today.totalTokens)) \(L("tokens"))",
.secondary))
entries.append(.text(
"7d: \(UsageFormatter.usdString(last7.costUSD)) · " +
"\(UsageFormatter.tokenCountString(last7.totalTokens)) \(L("tokens"))",
.secondary))
entries.append(.text(
"30d: \(UsageFormatter.usdString(last30.costUSD)) · " +
"\(UsageFormatter.tokenCountString(last30.totalTokens)) \(L("tokens"))",
.secondary))
if let topModel = usage.topModels.first?.name {
entries.append(.text("\(L("Top model")): \(topModel)", .secondary))
}
}
static func appendOpenRouterUsageSummary(
entries: inout [Entry],
usage: OpenRouterUsageSnapshot)
{
if let daily = usage.keyUsageDaily {
entries.append(.text("\(L("Today")): \(UsageFormatter.usdString(daily))", .secondary))
}
if let weekly = usage.keyUsageWeekly {
entries.append(.text("\(L("Week")): \(UsageFormatter.usdString(weekly))", .secondary))
}
if let monthly = usage.keyUsageMonthly {
entries.append(.text("\(L("Month")): \(UsageFormatter.usdString(monthly))", .secondary))
}
}
static func appendMistralUsageSummary(
entries: inout [Entry],
usage: MistralUsageSnapshot)
{
let latest = usage.daily.last
if let latest {
entries.append(.text(
"\(L("Latest")): \(usage.currencySymbol)\(String(format: "%.4f", max(0, latest.cost))) · " +
"\(UsageFormatter.tokenCountString(latest.totalTokens)) \(L("tokens"))",
.secondary))
}
let totalTokens = usage.totalInputTokens + usage.totalCachedTokens + usage.totalOutputTokens
entries.append(.text(
"\(L("Month")): \(usage.currencySymbol)\(String(format: "%.4f", max(0, usage.totalCost))) · " +
"\(UsageFormatter.tokenCountString(totalTokens)) \(L("tokens"))",
.secondary))
if let top = Self.topMistralModel(from: usage.daily) {
entries.append(.text("\(L("Top model")): \(top)", .secondary))
}
}
static func appendPoeUsageSummary(
entries: inout [Entry],
usage: PoeUsageHistorySnapshot)
{
let today = usage.currentDay()
let week = usage.last7Days
let month = usage.last30Days
let todayCostSuffix = today.costUSD.map { " · \(UsageFormatter.usdString($0))" } ?? ""
let weekCostSuffix = week.costUSD.map { " · \(UsageFormatter.usdString($0))" } ?? ""
let monthCostSuffix = month.costUSD.map { " · \(UsageFormatter.usdString($0))" } ?? ""
entries.append(.text(
"\(L("Today")): \(Self.pointsString(today.points)) · " +
"\(UsageFormatter.tokenCountString(today.requests)) \(L("requests"))\(todayCostSuffix)",
.secondary))
entries.append(.text(
"7d: \(Self.pointsString(week.points)) · " +
"\(UsageFormatter.tokenCountString(week.requests)) \(L("requests"))\(weekCostSuffix)",
.secondary))
entries.append(.text(
"30d: \(Self.pointsString(month.points)) · " +
"\(UsageFormatter.tokenCountString(month.requests)) \(L("requests"))\(monthCostSuffix)",
.secondary))
if let topModel = usage.topModels.first {
entries.append(
.text(
"\(L("Top model")): \(topModel.name) (\(Self.pointsString(topModel.points)))",
.secondary))
}
if !usage.topUsageTypes.isEmpty {
let summary = usage.topUsageTypes
.prefix(2)
.map { "\($0.name): \(Self.pointsString($0.points))" }
.joined(separator: " · ")
entries.append(.text("Usage mix: \(summary)", .secondary))
}
let recent = usage.recentEntries(limit: 3)
if !recent.isEmpty {
entries.append(.text("Recent activity:", .secondary))
for entry in recent {
let stamp = Self.poeTimeString(entry.createdAt)
entries.append(.text(
"\(stamp) · \(entry.model) · \(Self.pointsString(entry.points))",
.secondary))
}
}
}
private static func topMistralModel(from entries: [MistralDailyUsageBucket]) -> String? {
var tokens: [String: Int] = [:]
for entry in entries {
for model in entry.models {
tokens[model.name, default: 0] += model.totalTokens
}
}
return tokens.max {
if $0.value == $1.value {
return $0.key > $1.key
}
return $0.value < $1.value
}?.key
}
private static func pointsString(_ points: Double) -> String {
let value = max(0, points)
if value.rounded() == value {
return "\(UsageFormatter.tokenCountString(Int(value))) points"
}
return "\(String(format: "%.1f", value)) points"
}
private static func poeTimeString(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = .current
formatter.dateFormat = "MM-dd HH:mm"
return formatter.string(from: date)
}
}
@@ -0,0 +1,12 @@
import CodexBarCore
extension MenuDescriptor {
static func appendWayfinderUsageSummary(
entries: inout [Entry],
usage: WayfinderUsageSnapshot)
{
for line in usage.displayLines {
entries.append(.text(line, .secondary))
}
}
}
+752
View File
@@ -0,0 +1,752 @@
import CodexBarCore
import Foundation
@MainActor
struct MenuDescriptor {
struct SubmenuItem: Equatable {
let title: String
let action: MenuAction?
let isEnabled: Bool
let isChecked: Bool
init(title: String, action: MenuAction?, isEnabled: Bool = true, isChecked: Bool = false) {
self.title = title
self.action = action
self.isEnabled = isEnabled
self.isChecked = isChecked
}
}
struct Section {
var entries: [Entry]
}
enum Entry {
case text(String, TextStyle)
case action(String, MenuAction)
case unavailable(String, String?)
case submenu(String, String?, [SubmenuItem])
case divider
var isActionable: Bool {
switch self {
case .action, .submenu, .unavailable: true
case .text, .divider: false
}
}
}
enum MenuActionSystemImage: String {
case installUpdate = "arrow.down.circle"
case refresh = "arrow.clockwise"
case dashboard = "chart.xyaxis.line"
case statusPage = "waveform.path.ecg"
case changelog = "list.bullet.rectangle"
case addAccount = "plus"
case systemAccount = "person.crop.circle"
case switchAccount = "key"
case openTerminal = "terminal"
case loginToProvider = "arrow.right.square"
case settings = "gearshape"
case about = "info.circle"
case quit = "xmark.rectangle"
case copyError = "doc.on.doc"
}
enum TextStyle {
case headline
case primary
case secondary
}
enum MenuAction: Equatable {
case installUpdate
case refresh
case refreshAugmentSession
case dashboard
case statusPage
case changelog
case addCodexAccount
case requestCodexSystemPromotion(UUID)
case addProviderAccount(UsageProvider)
case switchAccount(UsageProvider)
case openTerminal(command: String)
case loginToProvider(url: String)
case settings
case about
case quit
case copyError(String)
case focusAgentSession(AgentSession, remoteHost: String?)
}
var sections: [Section]
static func build(
provider: UsageProvider?,
store: UsageStore,
settings: SettingsStore,
account: AccountInfo,
managedCodexAccountCoordinator: ManagedCodexAccountCoordinator? = nil,
codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator? = nil,
updateReady: Bool,
includeContextualActions: Bool = true,
agentSessionsEnabled: Bool = false,
localAgentSessions: [AgentSession] = [],
remoteAgentHosts: [RemoteSessionHostResult] = [],
now: Date = Date()) -> MenuDescriptor
{
var sections: [Section] = []
if let provider {
let fallbackAccount = store.accountInfo(for: provider)
sections.append(Self.usageSection(for: provider, store: store, settings: settings))
if let accountSection = Self.accountSection(
for: provider,
store: store,
settings: settings,
account: fallbackAccount)
{
sections.append(accountSection)
}
} else {
var addedUsage = false
for enabledProvider in store.enabledProviders() {
sections.append(Self.usageSection(for: enabledProvider, store: store, settings: settings))
addedUsage = true
}
if addedUsage {
if let accountProvider = Self.accountProviderForCombined(store: store),
let fallbackAccount = Optional(store.accountInfo(for: accountProvider)),
let accountSection = Self.accountSection(
for: accountProvider,
store: store,
settings: settings,
account: fallbackAccount)
{
sections.append(accountSection)
}
} else {
sections.append(Section(entries: [.text(L("No usage configured."), .secondary)]))
}
}
if includeContextualActions {
let actions = Self.actionsSection(
for: provider,
store: store,
account: account,
managedCodexAccountCoordinator: managedCodexAccountCoordinator,
codexAccountPromotionCoordinator: codexAccountPromotionCoordinator)
if !actions.entries.isEmpty {
sections.append(actions)
}
}
if agentSessionsEnabled {
sections.append(Self.agentSessionsSection(
localSessions: localAgentSessions,
remoteHosts: remoteAgentHosts,
now: now))
}
sections.append(Self.metaSection(updateReady: updateReady))
return MenuDescriptor(sections: sections)
}
static func agentSessionsSection(
localSessions: [AgentSession],
remoteHosts: [RemoteSessionHostResult],
now: Date = Date()) -> Section
{
let totalCount = localSessions.count + remoteHosts.reduce(0) { $0 + $1.sessions.count }
var entries: [Entry] = [.text("Agent Sessions (\(totalCount))", .headline)]
for session in localSessions {
entries.append(.action(
self.agentSessionRowTitle(session, now: now),
.focusAgentSession(session, remoteHost: nil)))
}
for remoteHost in remoteHosts {
if let error = remoteHost.error {
entries.append(.unavailable("\(remoteHost.host) — unreachable", error))
continue
}
entries.append(.text("\(remoteHost.host)\(remoteHost.sessions.count)", .secondary))
for session in remoteHost.sessions {
entries.append(.action(
self.agentSessionRowTitle(session, now: now),
.focusAgentSession(session, remoteHost: remoteHost.host)))
}
}
if totalCount == 0 {
entries.append(.unavailable("No agent sessions found", nil))
}
return Section(entries: entries)
}
private static func agentSessionRowTitle(_ session: AgentSession, now: Date) -> String {
let state = session.state == .active ? "" : ""
let providerGlyph = session.provider == .codex ? "" : ""
let project = session.projectName ?? "Unknown project"
return "\(state) \(providerGlyph) \(project)\(session.provider.rawValue) · " +
"\(session.source.rawValue) · \(self.agentSessionAge(session, now: now))"
}
private static func agentSessionAge(_ session: AgentSession, now: Date) -> String {
guard let activity = session.lastActivityAt ?? session.startedAt else { return "now" }
let seconds = max(0, Int(now.timeIntervalSince(activity)))
if seconds < 60 {
return "\(seconds)s"
}
if seconds < 3600 {
return "\(seconds / 60)m"
}
if seconds < 86400 {
return "\(seconds / 3600)h"
}
return "\(seconds / 86400)d"
}
private static func usageSection(
for provider: UsageProvider,
store: UsageStore,
settings: SettingsStore) -> Section
{
let meta = store.metadata(for: provider)
var entries: [Entry] = []
let headlineText: String = {
if let ver = Self.versionNumber(for: provider, store: store) {
return "\(meta.displayName) \(ver)"
}
return meta.displayName
}()
entries.append(.text(headlineText, .headline))
if let snap = store.snapshot(for: provider) {
let resetStyle = settings.resetTimeDisplayStyle
let labels = Self.rateWindowLabels(provider: provider, metadata: meta, snapshot: snap)
if let primary = snap.primary {
let primaryDetail = primary.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines)
let primaryDescriptionIsDetail = provider == .warp || provider == .kilo || provider == .abacus ||
provider == .deepseek || provider == .azureopenai || provider == .mimo || provider == .qoder ||
provider == .sub2api
let primaryWindow = if primaryDescriptionIsDetail {
// Some providers use resetDescription for non-reset detail
// (e.g., "Unlimited", "X/Y credits"). Avoid rendering it as a "Resets ..." line.
RateWindow(
usedPercent: primary.usedPercent,
windowMinutes: primary.windowMinutes,
resetsAt: primary.resetsAt,
resetDescription: nil)
} else {
primary
}
Self.appendRateWindow(
entries: &entries,
title: labels.primary,
window: primaryWindow,
resetStyle: resetStyle,
showUsed: settings.usageBarsShowUsed)
if primaryDescriptionIsDetail,
let primaryDetail,
!primaryDetail.isEmpty
{
entries.append(.text(primaryDetail, .secondary))
}
if provider == .crof,
primary.resetsAt != nil,
let detail = primary.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines),
!detail.isEmpty
{
entries.append(.text(detail, .secondary))
}
if provider == .abacus,
let pace = store.weeklyPace(provider: provider, window: primary)
{
let paceSummary = UsagePaceText.weeklySummary(provider: provider, pace: pace)
entries.append(.text(paceSummary, .secondary))
}
if let paceSummary = UsagePaceText.sessionSummary(provider: provider, window: primary) {
entries.append(.text(paceSummary, .secondary))
}
}
if let weekly = snap.secondary {
let weeklyResetOverride: String? = {
guard provider == .warp || provider == .kilo || provider == .perplexity || provider == .crof ||
provider == .sub2api
else { return nil }
let detail = weekly.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines)
guard let detail, !detail.isEmpty else { return nil }
if provider == .kilo, weekly.resetsAt != nil {
return nil
}
return detail
}()
Self.appendRateWindow(
entries: &entries,
title: labels.secondary,
window: weekly,
resetStyle: resetStyle,
showUsed: settings.usageBarsShowUsed,
resetOverride: weeklyResetOverride)
if provider == .kilo,
weekly.resetsAt != nil,
let detail = weekly.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines),
!detail.isEmpty
{
entries.append(.text(detail, .secondary))
}
if let pace = store.weeklyPace(provider: provider, window: weekly) {
let paceSummary = UsagePaceText.weeklySummary(provider: provider, pace: pace)
entries.append(.text(paceSummary, .secondary))
}
}
if labels.showsTertiary, let opus = snap.tertiary {
// Perplexity purchased credits don't reset; show the balance as plain text.
let opusResetOverride: String? = provider == .perplexity || provider == .sub2api
? opus.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines)
: nil
Self.appendRateWindow(
entries: &entries,
title: labels.tertiary,
window: opus,
resetStyle: resetStyle,
showUsed: settings.usageBarsShowUsed,
resetOverride: opusResetOverride)
}
Self.appendProviderUsageSummaries(
entries: &entries,
snapshot: snap,
showOptionalUsage: settings.showOptionalCreditsAndExtraUsage)
if snap.rateLimitsUnavailable(for: provider) {
entries.append(.text(L("Limits not available"), .secondary))
}
} else if !store.isStale(provider: provider),
store.knownLimitsAvailability(for: provider)?.isUnavailable == true
{
entries.append(.text(L("Limits not available"), .secondary))
} else {
entries.append(.text(L("No usage yet"), .secondary))
}
let usageContext = ProviderMenuUsageContext(
provider: provider,
store: store,
settings: settings,
metadata: meta,
snapshot: store.snapshot(for: provider))
ProviderCatalog.implementation(for: provider)?
.appendUsageMenuEntries(context: usageContext, entries: &entries)
return Section(entries: entries)
}
private static func appendProviderUsageSummaries(
entries: inout [Entry],
snapshot: UsageSnapshot,
showOptionalUsage: Bool)
{
if let cost = snapshot.providerCost {
if cost.currencyCode == "Quota" {
let used = String(format: "%.0f", cost.used)
let limit = String(format: "%.0f", cost.limit)
entries.append(.text("\(L("Quota")): \(used) / \(limit)", .primary))
}
}
if let openAIAPIUsage = snapshot.openAIAPIUsage {
Self.appendOpenAIAPIUsageSummary(entries: &entries, usage: openAIAPIUsage)
}
if let claudeAdminAPIUsage = snapshot.claudeAdminAPIUsage {
Self.appendClaudeAdminAPIUsageSummary(entries: &entries, usage: claudeAdminAPIUsage)
}
if let openRouterUsage = snapshot.openRouterUsage {
Self.appendOpenRouterUsageSummary(entries: &entries, usage: openRouterUsage)
}
if let clawRouterUsage = snapshot.clawRouterUsage {
entries.append(.text(
"\(UsageFormatter.tokenCountString(clawRouterUsage.requestCount)) \(L("requests")) · " +
"\(UsageFormatter.tokenCountString(clawRouterUsage.totalTokens)) \(L("tokens"))",
.secondary))
if !clawRouterUsage.providers.isEmpty {
let mix = clawRouterUsage.providers.prefix(5)
.map { "\($0.provider): \(UsageFormatter.tokenCountString($0.requestCount))" }
.joined(separator: " · ")
entries.append(.text("Routed providers: \(mix)", .secondary))
}
}
if let wayfinderUsage = snapshot.wayfinderUsage {
Self.appendWayfinderUsageSummary(entries: &entries, usage: wayfinderUsage)
}
if let poeUsage = snapshot.poeUsage, !poeUsage.daily.isEmpty {
Self.appendPoeUsageSummary(entries: &entries, usage: poeUsage)
}
if let mistralUsage = snapshot.mistralUsage, !mistralUsage.daily.isEmpty {
Self.appendMistralUsageSummary(entries: &entries, usage: mistralUsage)
}
if let mimoUsage = snapshot.mimoUsage {
entries.append(.text("\(L("Balance")): \(mimoUsage.balanceDetail)", .primary))
}
// Sakana pay-as-you-go is optional data gated by "Show optional credits and extra usage".
// Gate the render on the setting too, not just the fetch: toggling the setting off only
// rebuilds the menu, it does not immediately refetch, so a previously-populated
// sakanaPayAsYouGo would otherwise linger in the cached snapshot until the next refresh.
if showOptionalUsage, let sakanaPayAsYouGo = snapshot.sakanaPayAsYouGo {
entries.append(.text("\(L("Balance")): \(sakanaPayAsYouGo.balanceDetail)", .primary))
if let periodUsageTotal = sakanaPayAsYouGo.periodUsageTotal {
entries.append(.text(
"\(L("Usage")): \(UsageFormatter.usdString(periodUsageTotal))",
.secondary))
}
}
}
private static func accountSection(
for provider: UsageProvider,
store: UsageStore,
settings: SettingsStore,
account: AccountInfo) -> Section?
{
let snapshot = store.snapshot(for: provider)
let metadata = store.metadata(for: provider)
let entries = Self.accountEntries(
provider: provider,
snapshot: snapshot,
metadata: metadata,
fallback: account,
hidePersonalInfo: settings.hidePersonalInfo)
guard !entries.isEmpty else { return nil }
return Section(entries: entries)
}
private static func accountEntries(
provider: UsageProvider,
snapshot: UsageSnapshot?,
metadata: ProviderMetadata,
fallback: AccountInfo,
hidePersonalInfo: Bool) -> [Entry]
{
var entries: [Entry] = []
let emailText = snapshot?.accountEmail(for: provider)?
.trimmingCharacters(in: .whitespacesAndNewlines)
let loginMethodText = snapshot?.loginMethod(for: provider)?
.trimmingCharacters(in: .whitespacesAndNewlines)
let redactedEmail = PersonalInfoRedactor.redactEmail(emailText, isEnabled: hidePersonalInfo)
if let emailText, !emailText.isEmpty, !redactedEmail.isEmpty {
entries.append(.text("\(L("Account")): \(redactedEmail)", .secondary))
}
if provider == .kiro {
if let plan = snapshot?.kiroUsage?.displayPlanName,
!plan.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
entries.append(.text("\(L("Plan")): \(plan)", .secondary))
}
if let loginMethodText, !loginMethodText.isEmpty {
entries.append(.text("\(L("Auth")): \(loginMethodText)", .secondary))
}
if let overages = snapshot?.kiroUsage?.overagesStatus,
!overages.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
entries.append(.text("\(L("Overages")): \(overages)", .secondary))
}
} else if provider == .kilo {
let kiloLogin = self.kiloLoginParts(loginMethod: loginMethodText)
if let pass = kiloLogin.pass {
entries.append(.text("\(L("Plan")): \(AccountFormatter.plan(pass, provider: provider))", .secondary))
}
for detail in kiloLogin.details {
entries.append(.text("\(L("Activity")): \(detail)", .secondary))
}
} else if provider == .crossmodel {
if let loginMethodText, !loginMethodText.isEmpty {
entries.append(.text("\(L("Auth")): \(loginMethodText)", .secondary))
}
} else if let loginMethodText, !loginMethodText.isEmpty {
if provider == .openrouter || provider == .mimo || provider == .poe,
loginMethodText.localizedCaseInsensitiveContains("balance:")
{
let balanceValue = loginMethodText
.replacingOccurrences(
of: #"(?i)^\s*balance:\s*"#,
with: "",
options: [.regularExpression])
.trimmingCharacters(in: .whitespacesAndNewlines)
let value = balanceValue.isEmpty ? loginMethodText : balanceValue
entries.append(
.text("\(L("Balance")): \(AccountFormatter.plan(value, provider: provider))", .secondary))
} else {
entries.append(
.text(
"\(L("Plan")): \(AccountFormatter.plan(loginMethodText, provider: provider))",
.secondary))
}
}
if metadata.usesAccountFallback {
if emailText?.isEmpty ?? true, let fallbackEmail = fallback.email, !fallbackEmail.isEmpty {
let redacted = PersonalInfoRedactor.redactEmail(fallbackEmail, isEnabled: hidePersonalInfo)
if !redacted.isEmpty {
entries.append(.text("\(L("Account")): \(redacted)", .secondary))
}
}
if loginMethodText?.isEmpty ?? true, let fallbackPlan = fallback.plan, !fallbackPlan.isEmpty {
entries.append(
.text(
"\(L("Plan")): \(AccountFormatter.plan(fallbackPlan, provider: provider))",
.secondary))
}
}
return entries
}
private static func kiloLoginParts(loginMethod: String?) -> (pass: String?, details: [String]) {
guard let loginMethod else {
return (nil, [])
}
let parts = loginMethod
.components(separatedBy: "·")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
guard !parts.isEmpty else {
return (nil, [])
}
let first = parts[0]
if self.isKiloActivitySegment(first) {
return (nil, parts)
}
return (first, Array(parts.dropFirst()))
}
private static func isKiloActivitySegment(_ text: String) -> Bool {
let normalized = text.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
return normalized.hasPrefix("auto top-up:")
}
private static func accountProviderForCombined(store: UsageStore) -> UsageProvider? {
for provider in store.enabledProviders() {
let metadata = store.metadata(for: provider)
if store.snapshot(for: provider)?.identity(for: provider) != nil {
return provider
}
if metadata.usesAccountFallback {
return provider
}
}
return nil
}
private static func actionsSection(
for provider: UsageProvider?,
store: UsageStore,
account: AccountInfo,
managedCodexAccountCoordinator: ManagedCodexAccountCoordinator?,
codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator?) -> Section
{
var entries: [Entry] = []
let targetProvider = provider ?? store.enabledProviders().first
let metadata = targetProvider.map { store.metadata(for: $0) }
let fallbackAccount = targetProvider.map { store.accountInfo(for: $0) } ?? account
let loginContext = targetProvider.map {
ProviderMenuLoginContext(
provider: $0,
store: store,
settings: store.settings,
account: fallbackAccount)
}
// Show "Add Account" if no account, "Switch Account" if logged in
if let targetProvider,
let implementation = ProviderCatalog.implementation(for: targetProvider),
implementation.supportsLoginFlow
{
if let loginContext,
let override = implementation.loginMenuAction(context: loginContext)
{
entries.append(.action(override.label, override.action))
} else {
let loginAction = self.switchAccountTarget(for: provider, store: store)
let hasAccount = self.hasAccount(for: provider, store: store, account: fallbackAccount)
let accountLabel = hasAccount ? L("Switch Account...") : L("Add Account...")
entries.append(.action(accountLabel, loginAction))
}
}
if let targetProvider {
let actionContext = ProviderMenuActionContext(
provider: targetProvider,
store: store,
settings: store.settings,
account: fallbackAccount,
managedCodexAccountCoordinator: managedCodexAccountCoordinator,
codexAccountPromotionCoordinator: codexAccountPromotionCoordinator)
ProviderCatalog.implementation(for: targetProvider)?
.appendActionMenuEntries(context: actionContext, entries: &entries)
}
if metadata?.dashboardURL != nil {
entries.append(.action(L("Usage Dashboard"), .dashboard))
}
if metadata?.statusPageURL != nil || metadata?.statusLinkURL != nil {
entries.append(.action(L("Status Page"), .statusPage))
}
if store.settings.providerChangelogLinksEnabled, metadata?.changelogURL != nil {
entries.append(.action(L("Changelog"), .changelog))
}
if let statusLine = self.statusLine(for: provider, store: store) {
entries.append(.text(statusLine, .secondary))
}
return Section(entries: entries)
}
private static func metaSection(updateReady: Bool) -> Section {
var entries: [Entry] = []
if updateReady {
entries.append(.action(L("Update ready, restart now?"), .installUpdate))
}
entries.append(contentsOf: [
.action(L("Refresh"), .refresh),
.action(L("Settings..."), .settings),
.action(L("About CodexBar"), .about),
.action(L("Quit"), .quit),
])
return Section(entries: entries)
}
private static func statusLine(for provider: UsageProvider?, store: UsageStore) -> String? {
let target = provider ?? store.enabledProviders().first
guard let target,
let status = store.status(for: target),
status.indicator != .none else { return nil }
let description = status.description?.trimmingCharacters(in: .whitespacesAndNewlines)
let label = description?.isEmpty == false ? description! : status.indicator.label
if let updated = status.updatedAt {
let freshness = UsageFormatter.updatedString(from: updated)
return "\(label)\(freshness)"
}
return label
}
private static func switchAccountTarget(for provider: UsageProvider?, store: UsageStore) -> MenuAction {
if let provider {
return .switchAccount(provider)
}
if let enabled = store.enabledProviders().first {
return .switchAccount(enabled)
}
return .switchAccount(.codex)
}
private static func hasAccount(for provider: UsageProvider?, store: UsageStore, account: AccountInfo) -> Bool {
let target = provider ?? store.enabledProviders().first ?? .codex
if let email = store.snapshot(for: target)?.accountEmail(for: target),
!email.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
return true
}
let metadata = store.metadata(for: target)
if metadata.usesAccountFallback,
let fallback = account.email?.trimmingCharacters(in: .whitespacesAndNewlines),
!fallback.isEmpty
{
return true
}
return false
}
private static func rateWindowLabels(
provider: UsageProvider,
metadata: ProviderMetadata,
snapshot: UsageSnapshot) -> (primary: String, secondary: String, tertiary: String, showsTertiary: Bool)
{
if provider == .factory, snapshot.tertiary != nil {
return ("5-hour", L("Weekly"), L("Monthly"), true)
}
let primaryLabel = if provider == .grok {
GrokProviderDescriptor.primaryLabel(window: snapshot.primary) ?? metadata.sessionLabel
} else if provider == .doubao {
DoubaoProviderDescriptor.primaryLabel(window: snapshot.primary) ?? metadata.sessionLabel
} else if provider == .sub2api {
Sub2APIProviderDescriptor.primaryLabel(details: snapshot.sub2APIUsage) ?? metadata.sessionLabel
} else {
metadata.sessionLabel
}
return (
L(primaryLabel),
L(metadata.weeklyLabel),
metadata.opusLabel.map(L) ?? L("Sonnet"),
metadata.supportsOpus)
}
private static func appendRateWindow(
entries: inout [Entry],
title: String,
window: RateWindow,
resetStyle: ResetTimeDisplayStyle,
showUsed: Bool,
resetOverride: String? = nil)
{
let line = UsageFormatter
.usageLine(remaining: window.remainingPercent, used: window.usedPercent, showUsed: showUsed)
entries.append(.text("\(title): \(line)", .primary))
if let resetOverride {
entries.append(.text(resetOverride, .secondary))
} else if let reset = UsageFormatter.resetLine(for: window, style: resetStyle) {
entries.append(.text(reset, .secondary))
}
}
private static func versionNumber(for provider: UsageProvider, store: UsageStore) -> String? {
guard let raw = store.version(for: provider) else { return nil }
let pattern = #"[0-9]+(?:\.[0-9]+)*"#
guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil }
let range = NSRange(raw.startIndex..<raw.endIndex, in: raw)
guard let match = regex.firstMatch(in: raw, options: [], range: range),
let r = Range(match.range, in: raw) else { return nil }
return String(raw[r])
}
}
private enum AccountFormatter {
static func plan(_ text: String, provider: UsageProvider) -> String {
let cleaned = if provider == .codex {
CodexPlanFormatting.displayName(text) ?? UsageFormatter.cleanPlanName(text)
} else {
UsageFormatter.cleanPlanName(text)
}
return cleaned.isEmpty ? text : cleaned
}
static func email(_ text: String) -> String {
text
}
}
extension MenuDescriptor.MenuAction {
var systemImageName: String? {
switch self {
case .installUpdate: MenuDescriptor.MenuActionSystemImage.installUpdate.rawValue
case .settings: MenuDescriptor.MenuActionSystemImage.settings.rawValue
case .about: MenuDescriptor.MenuActionSystemImage.about.rawValue
case .quit: MenuDescriptor.MenuActionSystemImage.quit.rawValue
case .refresh: MenuDescriptor.MenuActionSystemImage.refresh.rawValue
case .refreshAugmentSession: MenuDescriptor.MenuActionSystemImage.refresh.rawValue
case .dashboard: MenuDescriptor.MenuActionSystemImage.dashboard.rawValue
case .statusPage: MenuDescriptor.MenuActionSystemImage.statusPage.rawValue
case .changelog: MenuDescriptor.MenuActionSystemImage.changelog.rawValue
case .addCodexAccount, .addProviderAccount: MenuDescriptor.MenuActionSystemImage.addAccount.rawValue
case .requestCodexSystemPromotion:
nil
case .switchAccount: MenuDescriptor.MenuActionSystemImage.switchAccount.rawValue
case .openTerminal: MenuDescriptor.MenuActionSystemImage.openTerminal.rawValue
case .loginToProvider: MenuDescriptor.MenuActionSystemImage.loginToProvider.rawValue
case .copyError: MenuDescriptor.MenuActionSystemImage.copyError.rawValue
case .focusAgentSession:
nil
}
}
}
+39
View File
@@ -0,0 +1,39 @@
import SwiftUI
extension EnvironmentValues {
@Entry var menuItemHighlighted: Bool = false
/// Optional live-refresh monitor injected into menu card views so the provider card
/// subtitle can reflect the in-flight "Refreshing" state in place while the NSMenu
/// stays open, without rebuilding the menu during AppKit tracking.
@Entry var menuCardRefreshMonitor: MenuCardRefreshMonitor?
}
enum MenuHighlightStyle {
static let selectionText = Color(nsColor: .selectedMenuItemTextColor)
static let normalPrimaryText = Color(nsColor: .controlTextColor)
static let normalSecondaryText = Color(nsColor: .secondaryLabelColor)
static func primary(_ highlighted: Bool) -> Color {
highlighted ? self.selectionText : self.normalPrimaryText
}
static func secondary(_ highlighted: Bool) -> Color {
highlighted ? self.selectionText : self.normalSecondaryText
}
static func error(_ highlighted: Bool) -> Color {
highlighted ? self.selectionText : Color(nsColor: .systemRed)
}
static func progressTrack(_ highlighted: Bool) -> Color {
highlighted ? self.selectionText.opacity(0.22) : Color(nsColor: .tertiaryLabelColor).opacity(0.22)
}
static func progressTint(_ highlighted: Bool, fallback: Color) -> Color {
highlighted ? self.selectionText : fallback
}
static func selectionBackground(_ highlighted: Bool) -> Color {
highlighted ? Color(nsColor: .selectedContentBackgroundColor) : .clear
}
}
@@ -0,0 +1,41 @@
import CodexBarCore
struct MenuOpenRefreshPlan: Equatable {
struct Inputs {
let refreshAllOnOpen: Bool
let enabledProviders: [UsageProvider]
let visibleProviders: [UsageProvider]
let refreshingProviders: Set<UsageProvider>
let staleProviders: Set<UsageProvider>
let missingProviders: Set<UsageProvider>
}
enum Scheduling: Equatable {
case sequential
case concurrent
}
let providers: [UsageProvider]
let scheduling: Scheduling
let refreshCodexDashboard: Bool
static func resolve(_ inputs: Inputs) -> Self {
if inputs.refreshAllOnOpen {
return Self(
providers: inputs.enabledProviders,
scheduling: .concurrent,
refreshCodexDashboard: inputs.enabledProviders.contains(.codex))
}
let enabled = Set(inputs.enabledProviders)
let providers = inputs.visibleProviders.filter {
enabled.contains($0) &&
(inputs.refreshingProviders.contains($0) || inputs.staleProviders.contains($0) ||
inputs.missingProviders.contains($0))
}
return Self(
providers: providers,
scheduling: .sequential,
refreshCodexDashboard: false)
}
}
@@ -0,0 +1,211 @@
struct MenuSessionCoordinator<MenuID: Hashable> {
enum ClosedPreparationPlan: Equatable {
case none
case nonDeferred
case required(version: Int)
}
private(set) var contentVersion = 0
private(set) var latestRequiredRebuildVersion = 0
private(set) var latestDataOnlyContentVersion = 0
private(set) var latestStructuralContentVersion = 0
private(set) var renderedVersions: [MenuID: Int] = [:]
private(set) var deferredUntilNextOpen: Set<MenuID> = []
private(set) var parentRebuildsDeferredDuringTracking: Set<MenuID> = []
private var nextMenuInteractionGeneration = 0
private(set) var menuInteractionGenerations: [MenuID: Int] = [:]
private var nextViewportRestoreGeneration = 0
private(set) var pendingViewportRestores: [MenuID: Int] = [:]
@discardableResult
mutating func invalidate(
allowsStaleContent: Bool,
requiresRebuild: Bool)
-> Int
{
self.contentVersion &+= 1
if allowsStaleContent {
self.latestDataOnlyContentVersion = self.contentVersion
} else {
self.latestStructuralContentVersion = self.contentVersion
if requiresRebuild {
self.latestRequiredRebuildVersion = self.contentVersion
}
}
return self.contentVersion
}
func needsRefresh(_ menuID: MenuID) -> Bool {
self.renderedVersions[menuID] != self.contentVersion
}
mutating func markFresh(_ menuID: MenuID) {
self.renderedVersions[menuID] = self.contentVersion
}
func renderedVersion(for menuID: MenuID) -> Int? {
self.renderedVersions[menuID]
}
func canPreserveStaleContent(for menuID: MenuID) -> Bool {
guard let renderedVersion = self.renderedVersions[menuID] else { return false }
return self.contentVersion == self.latestDataOnlyContentVersion &&
renderedVersion >= self.latestStructuralContentVersion
}
func hasRequiredClosedPreparation(for menuIDs: some Sequence<MenuID>) -> Bool {
guard self.latestRequiredRebuildVersion > 0 else { return false }
return menuIDs.contains { self.isRenderedVersion($0, olderThan: self.latestRequiredRebuildVersion) }
}
func closedPreparationPlan(for menuIDs: some Sequence<MenuID>) -> ClosedPreparationPlan {
if self.hasRequiredClosedPreparation(for: menuIDs) {
return .required(version: self.latestRequiredRebuildVersion)
}
if self.contentVersion > self.latestRequiredRebuildVersion {
return .none
}
return .nonDeferred
}
func isRenderedVersion(_ menuID: MenuID, olderThan version: Int) -> Bool {
(self.renderedVersions[menuID] ?? -1) < version
}
mutating func deferUntilNextOpen(_ menuID: MenuID) {
self.deferredUntilNextOpen.insert(menuID)
}
mutating func clearNextOpenDeferral(_ menuID: MenuID) {
self.deferredUntilNextOpen.remove(menuID)
}
func isDeferredUntilNextOpen(_ menuID: MenuID) -> Bool {
self.deferredUntilNextOpen.contains(menuID)
}
mutating func deferParentRebuild(_ menuID: MenuID) {
self.parentRebuildsDeferredDuringTracking.insert(menuID)
}
mutating func clearParentRebuildDeferral(_ menuID: MenuID) {
self.parentRebuildsDeferredDuringTracking.remove(menuID)
}
func isParentRebuildDeferred(_ menuID: MenuID) -> Bool {
self.parentRebuildsDeferredDuringTracking.contains(menuID)
}
/// Identifies one concrete open/close lifetime even when AppKit reuses the same menu object.
@discardableResult
mutating func beginTrackingSession(_ menuID: MenuID) -> Int {
self.replaceMenuInteractionGeneration(for: menuID)
}
func menuInteractionGeneration(for menuID: MenuID) -> Int? {
self.menuInteractionGenerations[menuID]
}
func isCurrentMenuInteraction(_ generation: Int, for menuID: MenuID) -> Bool {
self.menuInteractionGenerations[menuID] == generation
}
@discardableResult
mutating func advanceMenuInteraction(for menuID: MenuID) -> Int? {
guard self.menuInteractionGenerations[menuID] != nil else { return nil }
return self.replaceMenuInteractionGeneration(for: menuID)
}
private mutating func replaceMenuInteractionGeneration(for menuID: MenuID) -> Int {
self.nextMenuInteractionGeneration &+= 1
self.menuInteractionGenerations[menuID] = self.nextMenuInteractionGeneration
return self.nextMenuInteractionGeneration
}
mutating func endTrackingSession(_ menuID: MenuID) {
self.menuInteractionGenerations.removeValue(forKey: menuID)
}
/// One-shot viewport restore tied to the menu-tracking session that started a manual refresh.
@discardableResult
mutating func armViewportRestore(_ menuID: MenuID) -> Int {
self.nextViewportRestoreGeneration &+= 1
self.pendingViewportRestores[menuID] = self.nextViewportRestoreGeneration
return self.nextViewportRestoreGeneration
}
func isCurrentViewportRestore(_ generation: Int, for menuID: MenuID) -> Bool {
self.pendingViewportRestores[menuID] == generation
}
@discardableResult
mutating func consumeViewportRestore(_ menuID: MenuID, generation: Int) -> Bool {
guard self.isCurrentViewportRestore(generation, for: menuID) else { return false }
self.pendingViewportRestores.removeValue(forKey: menuID)
return true
}
mutating func cancelViewportRestore(_ menuID: MenuID) {
self.pendingViewportRestores.removeValue(forKey: menuID)
}
mutating func removeMenu(_ menuID: MenuID) {
self.renderedVersions.removeValue(forKey: menuID)
self.deferredUntilNextOpen.remove(menuID)
self.parentRebuildsDeferredDuringTracking.remove(menuID)
self.endTrackingSession(menuID)
self.cancelViewportRestore(menuID)
}
mutating func clearMenuTracking() {
self.renderedVersions.removeAll(keepingCapacity: false)
self.deferredUntilNextOpen.removeAll(keepingCapacity: false)
self.parentRebuildsDeferredDuringTracking.removeAll(keepingCapacity: false)
self.menuInteractionGenerations.removeAll(keepingCapacity: false)
self.pendingViewportRestores.removeAll(keepingCapacity: false)
}
#if DEBUG
mutating func replaceContentVersionForTesting(_ version: Int) {
self.contentVersion = version
}
mutating func replaceRenderedVersionsForTesting(_ versions: [MenuID: Int]) {
self.renderedVersions = versions
}
mutating func replaceDeferredMenusForTesting(_ menuIDs: Set<MenuID>) {
self.deferredUntilNextOpen = menuIDs
}
#endif
}
struct MenuRebuildRequestRegistry<MenuID: Hashable> {
private var nextToken = 0
private(set) var tokens: [MenuID: Int] = [:]
mutating func replaceRequest(for menuID: MenuID) -> Int {
self.nextToken &+= 1
self.tokens[menuID] = self.nextToken
return self.nextToken
}
func isCurrent(_ token: Int, for menuID: MenuID) -> Bool {
self.tokens[menuID] == token
}
@discardableResult
mutating func finish(_ token: Int, for menuID: MenuID) -> Bool {
guard self.isCurrent(token, for: menuID) else { return false }
self.tokens.removeValue(forKey: menuID)
return true
}
mutating func cancel(for menuID: MenuID) {
self.tokens.removeValue(forKey: menuID)
}
mutating func cancelAll() {
self.tokens.removeAll(keepingCapacity: false)
}
}
+128
View File
@@ -0,0 +1,128 @@
import CodexBarCore
import Foundation
import Security
protocol MiniMaxAPITokenStoring: Sendable {
func loadToken() throws -> String?
func storeToken(_ token: String?) throws
}
enum MiniMaxAPITokenStoreError: LocalizedError {
case keychainStatus(OSStatus)
case invalidData
var errorDescription: String? {
switch self {
case let .keychainStatus(status):
"Keychain error: \(status)"
case .invalidData:
"Keychain returned invalid data."
}
}
}
struct KeychainMiniMaxAPITokenStore: MiniMaxAPITokenStoring {
private static let log = CodexBarLog.logger(LogCategories.minimaxAPITokenStore)
private let service = "com.steipete.CodexBar"
private let account = "minimax-api-token"
func loadToken() throws -> String? {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping token load")
return nil
}
var result: CFTypeRef?
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true,
]
if case .interactionRequired = KeychainAccessPreflight
.checkGenericPassword(service: self.service, account: self.account)
{
KeychainPromptHandler.handler?(KeychainPromptContext(
kind: .minimaxToken,
service: self.service,
account: self.account))
}
let status = KeychainSecurity.copyMatching(query as CFDictionary, &result)
if status == errSecItemNotFound {
return nil
}
guard status == errSecSuccess else {
Self.log.error("Keychain read failed: \(status)")
throw MiniMaxAPITokenStoreError.keychainStatus(status)
}
guard let data = result as? Data else {
throw MiniMaxAPITokenStoreError.invalidData
}
let token = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
if let token, !token.isEmpty {
return token
}
return nil
}
func storeToken(_ token: String?) throws {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping token store")
return
}
let cleaned = token?.trimmingCharacters(in: .whitespacesAndNewlines)
if cleaned == nil || cleaned?.isEmpty == true {
try self.deleteIfPresent()
return
}
let data = cleaned!.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let attributes: [String: Any] = [
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
]
let updateStatus = KeychainSecurity.update(query as CFDictionary, attributes as CFDictionary)
if updateStatus == errSecSuccess {
return
}
if updateStatus != errSecItemNotFound {
Self.log.error("Keychain update failed: \(updateStatus)")
throw MiniMaxAPITokenStoreError.keychainStatus(updateStatus)
}
var addQuery = query
for (key, value) in attributes {
addQuery[key] = value
}
let addStatus = KeychainSecurity.add(addQuery as CFDictionary, nil)
guard addStatus == errSecSuccess else {
Self.log.error("Keychain add failed: \(addStatus)")
throw MiniMaxAPITokenStoreError.keychainStatus(addStatus)
}
}
private func deleteIfPresent() throws {
guard !KeychainAccessGate.isDisabled else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let status = KeychainSecurity.delete(query as CFDictionary)
if status == errSecSuccess || status == errSecItemNotFound {
return
}
Self.log.error("Keychain delete failed: \(status)")
throw MiniMaxAPITokenStoreError.keychainStatus(status)
}
}
+133
View File
@@ -0,0 +1,133 @@
import CodexBarCore
import Foundation
import Security
protocol MiniMaxCookieStoring: Sendable {
func loadCookieHeader() throws -> String?
func storeCookieHeader(_ header: String?) throws
}
enum MiniMaxCookieStoreError: LocalizedError {
case keychainStatus(OSStatus)
case invalidData
var errorDescription: String? {
switch self {
case let .keychainStatus(status):
"Keychain error: \(status)"
case .invalidData:
"Keychain returned invalid data."
}
}
}
struct KeychainMiniMaxCookieStore: MiniMaxCookieStoring {
private static let log = CodexBarLog.logger(LogCategories.minimaxCookieStore)
private let service = "com.steipete.CodexBar"
private let account = "minimax-cookie"
func loadCookieHeader() throws -> String? {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping cookie load")
return nil
}
var result: CFTypeRef?
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true,
]
if case .interactionRequired = KeychainAccessPreflight
.checkGenericPassword(service: self.service, account: self.account)
{
KeychainPromptHandler.handler?(KeychainPromptContext(
kind: .minimaxCookie,
service: self.service,
account: self.account))
}
let status = KeychainSecurity.copyMatching(query as CFDictionary, &result)
if status == errSecItemNotFound {
return nil
}
guard status == errSecSuccess else {
Self.log.error("Keychain read failed: \(status)")
throw MiniMaxCookieStoreError.keychainStatus(status)
}
guard let data = result as? Data else {
throw MiniMaxCookieStoreError.invalidData
}
let header = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
if let header, !header.isEmpty {
return header
}
return nil
}
func storeCookieHeader(_ header: String?) throws {
guard !KeychainAccessGate.isDisabled else {
Self.log.debug("Keychain access disabled; skipping cookie store")
return
}
guard let raw = header?.trimmingCharacters(in: .whitespacesAndNewlines),
!raw.isEmpty
else {
try self.deleteIfPresent()
return
}
guard MiniMaxCookieHeader.normalized(from: raw) != nil else {
try self.deleteIfPresent()
return
}
let data = raw.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let attributes: [String: Any] = [
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
]
let updateStatus = KeychainSecurity.update(query as CFDictionary, attributes as CFDictionary)
if updateStatus == errSecSuccess {
return
}
if updateStatus != errSecItemNotFound {
Self.log.error("Keychain update failed: \(updateStatus)")
throw MiniMaxCookieStoreError.keychainStatus(updateStatus)
}
var addQuery = query
for (key, value) in attributes {
addQuery[key] = value
}
let addStatus = KeychainSecurity.add(addQuery as CFDictionary, nil)
guard addStatus == errSecSuccess else {
Self.log.error("Keychain add failed: \(addStatus)")
throw MiniMaxCookieStoreError.keychainStatus(addStatus)
}
}
private func deleteIfPresent() throws {
guard !KeychainAccessGate.isDisabled else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: self.service,
kSecAttrAccount as String: self.account,
]
let status = KeychainSecurity.delete(query as CFDictionary)
if status == errSecSuccess || status == errSecItemNotFound {
return
}
Self.log.error("Keychain delete failed: \(status)")
throw MiniMaxCookieStoreError.keychainStatus(status)
}
}
@@ -0,0 +1,69 @@
import AppKit
import SwiftUI
/// Lightweight NSView-based mouse tracking with local coordinates.
///
/// Why: SwiftUI's `onHover` doesn't provide location, but we want "hover a bar to see values" on macOS.
@MainActor
struct MouseLocationReader: NSViewRepresentable {
let onMoved: (CGPoint?) -> Void
func makeNSView(context: Context) -> TrackingView {
let view = TrackingView()
view.onMoved = self.onMoved
return view
}
func updateNSView(_ nsView: TrackingView, context: Context) {
nsView.onMoved = self.onMoved
}
final class TrackingView: NSView {
var onMoved: ((CGPoint?) -> Void)?
private var trackingArea: NSTrackingArea?
override var isFlipped: Bool {
true
}
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
self.window?.acceptsMouseMovedEvents = true
self.updateTrackingAreas()
}
override func updateTrackingAreas() {
super.updateTrackingAreas()
if let trackingArea {
self.removeTrackingArea(trackingArea)
}
let options: NSTrackingArea.Options = [
// NSMenu popups aren't "key windows", so `.activeInKeyWindow` would drop events and cause hover
// state to flicker. `.activeAlways` keeps tracking stable while the menu is open.
.activeAlways,
.inVisibleRect,
.mouseEnteredAndExited,
.mouseMoved,
]
let area = NSTrackingArea(rect: .zero, options: options, owner: self, userInfo: nil)
self.addTrackingArea(area)
self.trackingArea = area
}
override func mouseEntered(with event: NSEvent) {
super.mouseEntered(with: event)
self.onMoved?(self.convert(event.locationInWindow, from: nil))
}
override func mouseMoved(with event: NSEvent) {
super.mouseMoved(with: event)
self.onMoved?(self.convert(event.locationInWindow, from: nil))
}
override func mouseExited(with event: NSEvent) {
super.mouseExited(with: event)
self.onMoved?(nil)
}
}
}
@@ -0,0 +1,60 @@
import CodexBarCore
import Foundation
extension Notification.Name {
static let codexbarOpenSettings = Notification.Name("codexbarOpenSettings")
static let codexbarDebugBlinkNow = Notification.Name("codexbarDebugBlinkNow")
#if DEBUG
static let codexbarDebugSimulateMemoryPressure =
Notification.Name("com.steipete.codexbar.debug.simulateMemoryPressure")
#endif
static let codexbarSessionLimitReset = Notification.Name("codexbarSessionLimitReset")
static let codexbarWeeklyLimitReset = Notification.Name("codexbarWeeklyLimitReset")
static let codexbarProviderConfigDidChange = Notification.Name("codexbarProviderConfigDidChange")
static let codexbarQuotaWarningDidPost = Notification.Name("codexbarQuotaWarningDidPost")
}
@MainActor
final class SessionLimitResetEvent: NSObject {
let provider: UsageProvider
let accountIdentifier: String
let accountLabel: String?
let usedPercent: Double
init(provider: UsageProvider, accountIdentifier: String, accountLabel: String?, usedPercent: Double) {
self.provider = provider
self.accountIdentifier = accountIdentifier
self.accountLabel = accountLabel
self.usedPercent = usedPercent
}
}
@MainActor
final class WeeklyLimitResetEvent: NSObject {
let provider: UsageProvider
let accountIdentifier: String
let accountLabel: String?
let usedPercent: Double
init(provider: UsageProvider, accountIdentifier: String, accountLabel: String?, usedPercent: Double) {
self.provider = provider
self.accountIdentifier = accountIdentifier
self.accountLabel = accountLabel
self.usedPercent = usedPercent
}
}
@MainActor
final class QuotaWarningPostedEvent: NSObject {
let provider: UsageProvider
let window: QuotaWarningWindow
let threshold: Int
let postedAt: Date
init(provider: UsageProvider, window: QuotaWarningWindow, threshold: Int, postedAt: Date) {
self.provider = provider
self.window = window
self.threshold = threshold
self.postedAt = postedAt
}
}
@@ -0,0 +1,548 @@
import AppKit
import CodexBarCore
import WebKit
@MainActor
final class OpenAICreditsPurchaseWindowController: NSWindowController, WKNavigationDelegate, WKScriptMessageHandler {
private static let defaultSize = NSSize(width: 980, height: 760)
private static let logHandlerName = "codexbarLog"
private static let debugLogURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("codexbar-buy-credits.log")
private static let autoStartScript = """
(() => {
if (window.__codexbarAutoBuyCreditsStarted) return 'already';
const log = (...args) => {
try {
window.webkit?.messageHandlers?.codexbarLog?.postMessage(args);
} catch {}
};
const buttonSelector = 'button, a, [role="button"], input[type="button"], input[type="submit"]';
const isVisible = (el) => {
if (!el || !el.getBoundingClientRect) return false;
const rect = el.getBoundingClientRect();
if (rect.width < 2 || rect.height < 2) return false;
const style = window.getComputedStyle ? window.getComputedStyle(el) : null;
if (style) {
if (style.display === 'none' || style.visibility === 'hidden') return false;
if (parseFloat(style.opacity || '1') === 0) return false;
}
return true;
};
const textOf = el => {
const raw = el && (el.innerText || el.textContent) ? String(el.innerText || el.textContent) : '';
return raw.trim();
};
const matches = text => {
const lower = String(text || '').toLowerCase();
if (!lower.includes('credit')) return false;
return (
lower.includes('buy') ||
lower.includes('add') ||
lower.includes('purchase') ||
lower.includes('top up') ||
lower.includes('top-up')
);
};
const matchesAddMore = text => {
const lower = String(text || '').toLowerCase();
return lower.includes('add more');
};
const labelFor = el => {
if (!el) return '';
return textOf(el) || el.getAttribute('aria-label') || el.getAttribute('title') || el.value || '';
};
const summarize = el => {
if (!el) return null;
return {
tag: el.tagName,
type: el.getAttribute('type'),
role: el.getAttribute('role'),
label: labelFor(el),
aria: el.getAttribute('aria-label'),
disabled: isDisabled(el),
href: el.getAttribute('href'),
testId: el.getAttribute('data-testid'),
className: (el.className && String(el.className).slice(0, 120)) || ''
};
};
const collectButtons = () => {
const results = new Set();
const addAll = (root) => {
if (!root || !root.querySelectorAll) return;
root.querySelectorAll(buttonSelector).forEach(el => results.add(el));
};
addAll(document);
document.querySelectorAll('*').forEach(el => {
if (el.shadowRoot) addAll(el.shadowRoot);
});
document.querySelectorAll('iframe').forEach(frame => {
try {
const doc = frame.contentDocument;
if (!doc) return;
addAll(doc);
doc.querySelectorAll('*').forEach(el => {
if (el.shadowRoot) addAll(el.shadowRoot);
});
} catch {}
});
return Array.from(results);
};
const findDialogNextButton = () => {
const dialog = document.querySelector('[role=\"dialog\"], dialog, [aria-modal=\"true\"]');
if (!dialog) return null;
const buttons = Array.from(dialog.querySelectorAll(buttonSelector));
const labeled = buttons.filter(btn => labelFor(btn).toLowerCase().startsWith('next'));
const visible = labeled.find(isVisible);
return visible || labeled[0] || null;
};
const clickButton = (el) => {
if (!el) return false;
try {
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
} catch {
try {
el.click();
} catch {
return false;
}
}
return true;
};
const triggerPointerClick = (el) => {
if (!el) return false;
const rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
if (!rect) return false;
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
const events = ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'];
for (const type of events) {
try {
el.dispatchEvent(new MouseEvent(type, {
bubbles: true,
cancelable: true,
view: window,
clientX: x,
clientY: y
}));
} catch {
return false;
}
}
return true;
};
const pickLikelyButton = (buttons) => {
if (!buttons || buttons.length === 0) return null;
const labeled = buttons.find(btn => {
const label = labelFor(btn);
if (matches(label) || matchesAddMore(label)) return true;
const aria = String(btn.getAttribute('aria-label') || '').toLowerCase();
return aria.includes('credit') || aria.includes('buy') || aria.includes('add');
});
return labeled || buttons[0];
};
const findAddMoreButton = () => {
const buttons = collectButtons();
return buttons.find(btn => matchesAddMore(labelFor(btn))) || null;
};
const findNextButton = () => {
const dialogNext = findDialogNextButton();
if (dialogNext) return dialogNext;
const buttons = collectButtons();
const labeled = buttons.filter(btn => {
const label = labelFor(btn).toLowerCase();
return label === 'next' || label.startsWith('next ');
});
const visible = labeled.find(isVisible);
if (visible) return visible;
const submit = buttons.find(btn => btn.type && String(btn.type).toLowerCase() === 'submit' && isVisible(btn));
return submit || labeled[0] || null;
};
const isDisabled = (el) => {
if (!el) return true;
if (el.disabled) return true;
const ariaDisabled = String(el.getAttribute('aria-disabled') || '').toLowerCase();
if (ariaDisabled === 'true') return true;
if (el.classList && (el.classList.contains('disabled') || el.classList.contains('is-disabled'))) {
return true;
}
return false;
};
const forceClickElement = (el) => {
if (!el) return false;
const rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
if (rect) {
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
const target = document.elementFromPoint(x, y);
if (target) {
try {
target.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
return true;
} catch {
return false;
}
}
}
return false;
};
const requestSubmit = (el) => {
if (!el || !el.closest) return false;
const form = el.closest('form');
if (!form) return false;
if (typeof form.requestSubmit === 'function') {
form.requestSubmit(el);
return true;
}
if (typeof form.submit === 'function') {
form.submit();
return true;
}
return false;
};
const clickNextIfReady = (attempts) => {
const nextButton = findNextButton();
if (!nextButton) {
if (attempts && attempts % 5 === 0) log('next_missing', { attempts });
return false;
}
if (isDisabled(nextButton)) {
if (attempts && attempts % 5 === 0) log('next_disabled', summarize(nextButton));
return false;
}
if (!isVisible(nextButton)) {
if (attempts && attempts % 5 === 0) log('next_hidden', summarize(nextButton));
return false;
}
nextButton.focus?.();
if (requestSubmit(nextButton)) {
log('next_submit', summarize(nextButton));
return true;
}
if (triggerPointerClick(nextButton)) {
log('next_pointer', summarize(nextButton));
return true;
}
if (clickButton(nextButton)) {
log('next_click', summarize(nextButton));
return true;
}
return forceClickElement(nextButton);
};
const startNextPolling = (initialDelay = 500, interval = 500, maxAttempts = 90) => {
if (window.__codexbarNextPolling) return;
window.__codexbarNextPolling = true;
log('start_next_poll', { initialDelay, interval, maxAttempts });
setTimeout(() => {
let attempts = 0;
const nextTimer = setInterval(() => {
attempts += 1;
if (attempts % 5 === 0) {
const nextButton = findNextButton();
log('next_poll', {
attempts,
found: Boolean(nextButton),
summary: summarize(nextButton)
});
}
if (clickNextIfReady(attempts) || attempts >= maxAttempts) {
clearInterval(nextTimer);
}
}, interval);
}, initialDelay);
};
const observeNextButton = () => {
if (window.__codexbarNextObserver || !window.MutationObserver) return;
const observer = new MutationObserver(() => {
if (clickNextIfReady(1)) {
observer.disconnect();
window.__codexbarNextObserver = null;
}
});
observer.observe(document.body, { subtree: true, childList: true, attributes: true });
window.__codexbarNextObserver = observer;
};
const findCreditsCardButton = () => {
const nodes = Array.from(document.querySelectorAll('h1,h2,h3,div,span,p'));
const labelMatch = nodes.find(node => {
const lower = textOf(node).toLowerCase();
return lower === 'credits remaining' || (lower.includes('credits') && lower.includes('remaining'));
});
if (!labelMatch) return null;
let cur = labelMatch;
for (let i = 0; i < 6 && cur; i++) {
const buttons = Array.from(cur.querySelectorAll(buttonSelector));
const picked = pickLikelyButton(buttons);
if (picked) return picked;
cur = cur.parentElement;
}
return null;
};
const findAndClick = () => {
const addMoreButton = findAddMoreButton();
if (addMoreButton) {
log('add_more_click', summarize(addMoreButton));
clickButton(addMoreButton);
return true;
}
const cardButton = findCreditsCardButton();
if (!cardButton) return false;
log('credits_card_click', summarize(cardButton));
return clickButton(cardButton);
};
const logDialogButtons = () => {
const dialog = document.querySelector('[role=\"dialog\"], dialog, [aria-modal=\"true\"]');
if (dialog) {
const buttons = Array.from(dialog.querySelectorAll(buttonSelector)).map(summarize).filter(Boolean);
if (buttons.length) {
log('dialog_buttons', { count: buttons.length, buttons: buttons.slice(0, 6) });
}
const nextButton = findDialogNextButton();
if (nextButton) {
log('dialog_next', summarize(nextButton));
setTimeout(() => clickNextIfReady(1), 100);
}
return;
}
const candidates = collectButtons()
.map(summarize)
.filter(Boolean)
.filter(entry => {
const label = (entry.label || '').toLowerCase();
return label.includes('next')
|| label.includes('continue')
|| label.includes('confirm')
|| label.includes('buy');
});
if (candidates.length) {
log('button_candidates', { count: candidates.length, buttons: candidates.slice(0, 8) });
}
};
log('auto_start', { href: location.href, ready: document.readyState });
const iframeSources = Array.from(document.querySelectorAll('iframe'))
.map(frame => frame.getAttribute('src') || '')
.filter(Boolean)
.slice(0, 6);
if (iframeSources.length) {
log('iframes', iframeSources);
}
const shadowHostCount = Array.from(document.querySelectorAll('*')).filter(el => el.shadowRoot).length;
if (shadowHostCount > 0) {
log('shadow_roots', { count: shadowHostCount });
}
if (findAndClick()) {
window.__codexbarAutoBuyCreditsStarted = true;
startNextPolling();
observeNextButton();
logDialogButtons();
return 'clicked';
}
startNextPolling(500);
observeNextButton();
logDialogButtons();
let attempts = 0;
const maxAttempts = 14;
const timer = setInterval(() => {
attempts += 1;
if (findAndClick()) {
logDialogButtons();
startNextPolling();
clearInterval(timer);
return;
}
if (attempts >= maxAttempts) {
clearInterval(timer);
}
}, 500);
window.__codexbarAutoBuyCreditsStarted = true;
return 'scheduled';
})();
"""
private let logger = CodexBarLog.logger(LogCategories.creditsPurchase)
private var webView: WKWebView?
private var accountEmail: String?
private var cacheScope: CookieHeaderCache.Scope?
private var pendingAutoStart = false
private let logHandler = WeakScriptMessageHandler()
init() {
super.init(window: nil)
self.logHandler.delegate = self
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func show(
purchaseURL: URL,
accountEmail: String?,
cacheScope: CookieHeaderCache.Scope?,
autoStartPurchase: Bool)
{
guard Self.canOpenPurchaseWindow(accountEmail: accountEmail, cacheScope: cacheScope) else {
self.close()
self.accountEmail = nil
self.cacheScope = nil
self.logger.error("Buy credits blocked: scoped account email unavailable")
return
}
let normalizedEmail = Self.normalizeEmail(accountEmail)
if self.window == nil || normalizedEmail != self.accountEmail || cacheScope != self.cacheScope {
self.accountEmail = normalizedEmail
self.cacheScope = cacheScope
self.buildWindow()
}
Self.resetDebugLog()
let accountValue = normalizedEmail == nil ? "none" : "set"
let sanitizedURL = Self.sanitizedURLString(purchaseURL)
Self.appendDebugLog(
"show autoStart=\(autoStartPurchase) url=\(sanitizedURL) account=\(accountValue)")
self.logger.info("Buy credits window opened")
self.logger.debug("Auto-start purchase", metadata: ["enabled": autoStartPurchase ? "1" : "0"])
self.logger.debug("Purchase URL", metadata: ["url": sanitizedURL])
self.logger.debug("Account email", metadata: ["state": accountValue])
self.pendingAutoStart = autoStartPurchase
self.load(url: purchaseURL)
self.window?.center()
self.showWindow(nil)
NSApp.activate(ignoringOtherApps: true)
}
private func buildWindow() {
let config = WKWebViewConfiguration()
config.userContentController.add(self.logHandler, name: Self.logHandlerName)
config.websiteDataStore = OpenAIDashboardWebsiteDataStore.store(
forAccountEmail: self.accountEmail,
scope: self.cacheScope)
let webView = WKWebView(frame: .zero, configuration: config)
webView.navigationDelegate = self
webView.allowsBackForwardNavigationGestures = true
webView.translatesAutoresizingMaskIntoConstraints = false
let container = NSView(frame: .zero)
container.addSubview(webView)
NSLayoutConstraint.activate([
webView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
webView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
webView.topAnchor.constraint(equalTo: container.topAnchor),
webView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
])
let window = NSWindow(
contentRect: Self.defaultFrame(),
styleMask: [.titled, .closable, .resizable],
backing: .buffered,
defer: false)
window.title = "Buy Credits"
window.isReleasedWhenClosed = false
window.collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary]
window.contentView = container
window.center()
window.delegate = self
self.window = window
self.webView = webView
}
private func load(url: URL) {
guard let webView else { return }
let request = URLRequest(url: url)
webView.load(request)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
guard self.pendingAutoStart else { return }
self.pendingAutoStart = false
let currentURL = webView.url?.absoluteString ?? "unknown"
Self.appendDebugLog("didFinish url=\(currentURL)")
self.logger.debug("Buy credits navigation finished", metadata: ["url": currentURL])
webView.evaluateJavaScript(Self.autoStartScript) { [logger] result, error in
if let error {
Self.appendDebugLog("autoStart error=\(error.localizedDescription)")
logger.error("Auto-start purchase failed", metadata: ["error": error.localizedDescription])
return
}
if let result {
Self.appendDebugLog("autoStart result=\(String(describing: result))")
logger.debug("Auto-start purchase result", metadata: ["result": String(describing: result)])
}
}
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard message.name == Self.logHandlerName else { return }
let payload = String(describing: message.body)
Self.appendDebugLog("js \(payload)")
self.logger.debug("Auto-buy log", metadata: ["payload": payload])
}
private static func normalizeEmail(_ email: String?) -> String? {
guard let raw = email?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else { return nil }
return raw.lowercased()
}
static func canOpenPurchaseWindow(accountEmail: String?, cacheScope: CookieHeaderCache.Scope?) -> Bool {
cacheScope == nil || self.normalizeEmail(accountEmail) != nil
}
private static func defaultFrame() -> NSRect {
let visible = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 1200, height: 900)
let width = min(Self.defaultSize.width, visible.width * 0.92)
let height = min(Self.defaultSize.height, visible.height * 0.88)
let origin = NSPoint(x: visible.midX - width / 2, y: visible.midY - height / 2)
return NSRect(origin: origin, size: NSSize(width: width, height: height))
}
private static func appendDebugLog(_ message: String) {
let timestamp = ISO8601DateFormatter().string(from: Date())
let line = "[\(timestamp)] \(LogRedactor.redact(message))\n"
guard let data = line.data(using: .utf8) else { return }
if FileManager.default.fileExists(atPath: Self.debugLogURL.path) {
if let handle = try? FileHandle(forWritingTo: Self.debugLogURL) {
handle.seekToEndOfFile()
handle.write(data)
try? handle.close()
}
} else {
try? data.write(to: Self.debugLogURL, options: .atomic)
}
}
private static func resetDebugLog() {
try? FileManager.default.removeItem(at: self.debugLogURL)
}
private static func sanitizedURLString(_ url: URL) -> String {
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return url.absoluteString
}
components.query = nil
components.fragment = nil
return components.string ?? url.absoluteString
}
}
private final class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
weak var delegate: WKScriptMessageHandler?
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
self.delegate?.userContentController(userContentController, didReceive: message)
}
}
// MARK: - NSWindowDelegate
extension OpenAICreditsPurchaseWindowController: NSWindowDelegate {
func windowWillClose(_ notification: Notification) {
guard let window = self.window else { return }
let webView = self.webView
self.pendingAutoStart = false
self.webView = nil
self.window = nil
self.logger.info("Buy credits window closing")
WebKitTeardown.scheduleCleanup(owner: window, window: window, webView: webView)
}
}
@@ -0,0 +1,33 @@
import Foundation
enum PersonalInfoRedactor {
static let emailPlaceholder = ""
private static let emailRegex: NSRegularExpression? = {
let pattern = #"[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}"#
return try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive])
}()
static func redactEmail(_ email: String?, isEnabled: Bool) -> String {
guard let email, !email.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return "" }
guard isEnabled else { return email }
return Self.emailPlaceholder
}
static func redactEmails(in text: String?, isEnabled: Bool) -> String? {
guard let text else { return nil }
guard isEnabled else { return text }
guard let regex = Self.emailRegex else { return text }
let range = NSRange(text.startIndex..<text.endIndex, in: text)
guard regex.firstMatch(in: text, options: [], range: range) != nil else { return text }
let redacted = regex.stringByReplacingMatches(
in: text,
options: [],
range: range,
withTemplate: Self.emailPlaceholder)
return redacted
.replacingOccurrences(of: #"\s+([:.,;])"#, with: "$1", options: .regularExpression)
.replacingOccurrences(of: #"\s{2,}"#, with: " ", options: .regularExpression)
.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
@@ -0,0 +1,853 @@
import Charts
import CodexBarCore
import SwiftUI
@MainActor
struct PlanUtilizationHistoryChartMenuView: View {
private enum Layout {
static let chartHeight: CGFloat = 130
static let detailHeight: CGFloat = 16
static let emptyStateHeight: CGFloat = chartHeight + detailHeight
static let maxPoints = 30
static let maxAxisLabels = 4
static let barWidth: CGFloat = 6
}
private struct SeriesSelection: Hashable {
let name: PlanUtilizationSeriesName
let windowMinutes: Int
var id: String {
"\(self.name.rawValue):\(self.windowMinutes)"
}
}
private struct VisibleSeries: Identifiable, Equatable {
let selection: SeriesSelection
let title: String
let history: PlanUtilizationSeriesHistory
var id: String {
self.selection.id
}
}
private struct EntryPointAccumulator {
let effectiveBoundaryDate: Date
let displayBoundaryDate: Date
let observedAt: Date
let usedPercent: Double
let hasObservedResetBoundary: Bool
}
private struct ResetBoundaryLattice {
let referenceBoundaryDate: Date
let windowInterval: TimeInterval
}
private struct Point: Identifiable {
let id: Date
let index: Int
let date: Date
let usedPercent: Double
let isObserved: Bool
}
private struct Model {
let points: [Point]
let axisIndexes: [Double]
let xDomain: ClosedRange<Double>?
let pointsByID: [Date: Point]
let pointsByIndex: [Int: Point]
let barColor: Color
let trackColor: Color
}
private let provider: UsageProvider
private let visibleSeries: [VisibleSeries]
private let modelsBySeriesID: [String: Model]
private let emptyModel: Model
private let width: CGFloat
@State private var selectedSeriesID: String?
@State private var selectedPointID: Date?
init(
provider: UsageProvider,
histories: [PlanUtilizationSeriesHistory],
snapshot: UsageSnapshot? = nil,
width: CGFloat)
{
self.provider = provider
let visibleSeries = Self.visibleSeries(
histories: histories,
provider: provider,
snapshot: snapshot)
let referenceDate = Date()
self.visibleSeries = visibleSeries
self.modelsBySeriesID = Dictionary(uniqueKeysWithValues: visibleSeries.map {
($0.id, Self.makeModel(history: $0.history, provider: provider, referenceDate: referenceDate))
})
self.emptyModel = Self.emptyModel(provider: provider)
self.width = width
}
var body: some View {
let effectiveSelectedSeries = self.visibleSeries.first(where: { $0.id == self.selectedSeriesID })
?? self.visibleSeries.first
let model = effectiveSelectedSeries.flatMap { self.modelsBySeriesID[$0.id] } ?? self.emptyModel
VStack(alignment: .leading, spacing: 10) {
if self.visibleSeries.count > 1 {
Picker(selection: Binding(
get: { effectiveSelectedSeries?.id ?? "" },
set: { newValue in
self.selectedSeriesID = newValue
self.selectedPointID = nil
})) {
ForEach(self.visibleSeries) { series in
Text(series.title).tag(series.id)
}
} label: {
EmptyView()
}
.labelsHidden()
.pickerStyle(.segmented)
}
if model.points.isEmpty {
ZStack {
Text(Self.emptyStateText(title: effectiveSelectedSeries?.title))
.font(.footnote)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
.frame(height: Layout.emptyStateHeight)
} else {
self.utilizationChart(model: model)
.chartYAxis(.hidden)
.chartYScale(domain: 0...100)
.chartXAxis {
AxisMarks(values: model.axisIndexes) { value in
AxisGridLine().foregroundStyle(Color.clear)
AxisTick().foregroundStyle(Color.clear)
AxisValueLabel {
if let raw = value.as(Double.self) {
let index = Int(raw.rounded())
if let point = model.pointsByIndex[index] {
let isTrailingFullChartLabel = index == model.points.last?.index
&& model.points.count == Layout.maxPoints
Self.axisLabel(
for: point,
windowMinutes: effectiveSelectedSeries?.history.windowMinutes ?? 0,
isTrailingFullChartLabel: isTrailingFullChartLabel)
}
}
}
}
}
.chartLegend(.hidden)
.frame(height: Layout.chartHeight)
.accessibilityLabel(L("Plan utilization chart"))
.accessibilityValue(
model.points.isEmpty
? L("No data")
: String(format: L("%d utilization samples"), model.points.count))
.chartOverlay { proxy in
GeometryReader { geo in
MouseLocationReader { location in
self.updateSelection(location: location, model: model, proxy: proxy, geo: geo)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
}
}
Text(self.detailLine(model: model, windowMinutes: effectiveSelectedSeries?.history.windowMinutes ?? 0))
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.tail)
.frame(height: Layout.detailHeight, alignment: .leading)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 10)
.frame(minWidth: self.width, maxWidth: .infinity, alignment: .topLeading)
.task(id: self.visibleSeries.map(\.id).joined(separator: ",")) {
guard let firstVisibleSeries = self.visibleSeries.first else { return }
guard !self.visibleSeries.contains(where: { $0.id == self.selectedSeriesID }) else { return }
self.selectedSeriesID = firstVisibleSeries.id
self.selectedPointID = nil
}
}
private nonisolated static func visibleSeries(
histories: [PlanUtilizationSeriesHistory],
provider: UsageProvider,
snapshot: UsageSnapshot?) -> [VisibleSeries]
{
let metadata = ProviderDescriptorRegistry.metadata[provider]
let allowedNames = self.visibleSeriesNames(provider: provider, snapshot: snapshot)
var historiesBySelection: [SeriesSelection: PlanUtilizationSeriesHistory] = [:]
for history in histories {
guard !history.entries.isEmpty else { continue }
guard history.windowMinutes > 0 else { continue }
guard allowedNames?.contains(history.name) ?? true else { continue }
let canonicalWindowMinutes = history.name.canonicalWindowMinutes(history.windowMinutes)
let selection = SeriesSelection(name: history.name, windowMinutes: canonicalWindowMinutes)
if let existingHistory = historiesBySelection[selection] {
historiesBySelection[selection] = PlanUtilizationSeriesHistory(
name: history.name,
windowMinutes: canonicalWindowMinutes,
entries: Self.mergedEntries(existingHistory.entries + history.entries))
} else {
historiesBySelection[selection] = PlanUtilizationSeriesHistory(
name: history.name,
windowMinutes: canonicalWindowMinutes,
entries: history.entries)
}
}
return historiesBySelection.values
.sorted { lhs, rhs in
let lhsOrder = self.seriesSortOrder(lhs.name)
let rhsOrder = self.seriesSortOrder(rhs.name)
if lhsOrder != rhsOrder {
return lhsOrder < rhsOrder
}
if lhs.windowMinutes != rhs.windowMinutes {
return lhs.windowMinutes < rhs.windowMinutes
}
return lhs.name.rawValue < rhs.name.rawValue
}
.map { history in
VisibleSeries(
selection: SeriesSelection(name: history.name, windowMinutes: history.windowMinutes),
title: self.seriesTitle(name: history.name, metadata: metadata),
history: history)
}
}
nonisolated static func mergedEntries(
_ entries: [PlanUtilizationHistoryEntry]) -> [PlanUtilizationHistoryEntry]
{
var seen: Set<PlanUtilizationHistoryEntry> = []
return entries.filter { entry in
seen.insert(entry).inserted
}
}
private nonisolated static func visibleSeriesNames(
provider: UsageProvider,
snapshot: UsageSnapshot?) -> Set<PlanUtilizationSeriesName>?
{
guard let snapshot else { return nil }
var names: Set<PlanUtilizationSeriesName> = []
switch provider {
case .codex:
if snapshot.primary != nil { names.insert(.session) }
if snapshot.secondary != nil { names.insert(.weekly) }
case .claude:
if snapshot.primary != nil { names.insert(.session) }
if snapshot.secondary != nil { names.insert(.weekly) }
if snapshot.tertiary != nil,
ProviderDescriptorRegistry.metadata[provider]?.supportsOpus == true
{
names.insert(.opus)
}
default:
let windows = [snapshot.primary, snapshot.secondary, snapshot.tertiary].compactMap(\.self)
+ (snapshot.extraRateWindows?.filter(\.usageKnown).map(\.window) ?? [])
guard windows.contains(where: { $0.windowMinutes == 7 * 24 * 60 }) else { return nil }
names.insert(.weekly)
}
return names
}
private nonisolated static func makeModel(
history: PlanUtilizationSeriesHistory?,
provider: UsageProvider,
referenceDate: Date) -> Model
{
guard let history else {
return self.emptyModel(provider: provider)
}
var points = self.seriesPoints(history: history, referenceDate: referenceDate)
if points.count > Layout.maxPoints {
points = Array(points.suffix(Layout.maxPoints))
}
points = points.enumerated().map { offset, point in
Point(
id: point.id,
index: offset,
date: point.date,
usedPercent: point.usedPercent,
isObserved: point.isObserved)
}
let pointsByID = Dictionary(uniqueKeysWithValues: points.map { ($0.id, $0) })
let pointsByIndex = Dictionary(uniqueKeysWithValues: points.map { ($0.index, $0) })
let color = ProviderDescriptorRegistry.descriptor(for: provider).branding.color
let barColor = Color(red: color.red, green: color.green, blue: color.blue)
let trackColor = MenuHighlightStyle.progressTrack(false)
return Model(
points: points,
axisIndexes: self.axisIndexes(points: points, windowMinutes: history.windowMinutes),
xDomain: self.xDomain(points: points),
pointsByID: pointsByID,
pointsByIndex: pointsByIndex,
barColor: barColor,
trackColor: trackColor)
}
private nonisolated static func emptyModel(provider: UsageProvider) -> Model {
let color = ProviderDescriptorRegistry.descriptor(for: provider).branding.color
let barColor = Color(red: color.red, green: color.green, blue: color.blue)
let trackColor = MenuHighlightStyle.progressTrack(false)
return Model(
points: [],
axisIndexes: [],
xDomain: nil,
pointsByID: [:],
pointsByIndex: [:],
barColor: barColor,
trackColor: trackColor)
}
private nonisolated static func seriesPoints(
history: PlanUtilizationSeriesHistory,
referenceDate: Date) -> [Point]
{
guard history.windowMinutes > 0 else { return [] }
let windowInterval = Double(history.windowMinutes) * 60
let resetBoundaryLattice = self.resetBoundaryLattice(
entries: history.entries,
windowMinutes: history.windowMinutes)
var strongestObservedPointByPeriod: [Date: EntryPointAccumulator] = [:]
for entry in history.entries {
let candidate = self.observedPointCandidate(
for: entry,
windowMinutes: history.windowMinutes,
resetBoundaryLattice: resetBoundaryLattice)
if let existing = strongestObservedPointByPeriod[candidate.effectiveBoundaryDate],
!self.shouldPreferObservedPoint(candidate, over: existing)
{
continue
}
strongestObservedPointByPeriod[candidate.effectiveBoundaryDate] = candidate
}
guard !strongestObservedPointByPeriod.isEmpty else { return [] }
let sortedPeriodBoundaryDates = strongestObservedPointByPeriod.keys.sorted()
var points: [Point] = []
var previousPeriodBoundaryDate: Date?
for periodBoundaryDate in sortedPeriodBoundaryDates {
if let previousPeriodBoundaryDate {
var cursor = previousPeriodBoundaryDate.addingTimeInterval(windowInterval)
while cursor < periodBoundaryDate {
points.append(Point(
id: cursor,
index: 0,
date: cursor,
usedPercent: 0,
isObserved: false))
cursor = cursor.addingTimeInterval(windowInterval)
}
}
if let bucket = strongestObservedPointByPeriod[periodBoundaryDate] {
points.append(Point(
id: bucket.effectiveBoundaryDate,
index: 0,
date: bucket.displayBoundaryDate,
usedPercent: bucket.usedPercent,
isObserved: true))
}
previousPeriodBoundaryDate = periodBoundaryDate
}
if let lastObservedPeriodBoundaryDate = sortedPeriodBoundaryDates.last {
let currentPeriodBoundaryDate = self.currentPeriodBoundaryDate(
for: referenceDate,
windowMinutes: history.windowMinutes,
resetBoundaryLattice: resetBoundaryLattice)
if currentPeriodBoundaryDate > lastObservedPeriodBoundaryDate {
var cursor = lastObservedPeriodBoundaryDate.addingTimeInterval(windowInterval)
while cursor <= currentPeriodBoundaryDate {
points.append(Point(
id: cursor,
index: 0,
date: cursor,
usedPercent: 0,
isObserved: false))
cursor = cursor.addingTimeInterval(windowInterval)
}
}
}
return points
}
private nonisolated static func observedPointCandidate(
for entry: PlanUtilizationHistoryEntry,
windowMinutes: Int,
resetBoundaryLattice: ResetBoundaryLattice?) -> EntryPointAccumulator
{
let rawResetBoundaryDate = entry.resetsAt.map(self.normalizedBoundaryDate)
let effectiveBoundaryDate = self.effectivePeriodBoundaryDate(
for: entry,
windowMinutes: windowMinutes,
rawResetBoundaryDate: rawResetBoundaryDate,
resetBoundaryLattice: resetBoundaryLattice)
return EntryPointAccumulator(
effectiveBoundaryDate: effectiveBoundaryDate,
displayBoundaryDate: rawResetBoundaryDate ?? effectiveBoundaryDate,
observedAt: entry.capturedAt,
usedPercent: max(0, min(100, entry.usedPercent)),
hasObservedResetBoundary: rawResetBoundaryDate != nil)
}
private nonisolated static func resetBoundaryLattice(
entries: [PlanUtilizationHistoryEntry],
windowMinutes: Int) -> ResetBoundaryLattice?
{
guard let latestObservedResetBoundaryDate = entries
.compactMap(\.resetsAt)
.map(self.normalizedBoundaryDate)
.max()
else {
return nil
}
return ResetBoundaryLattice(
referenceBoundaryDate: latestObservedResetBoundaryDate,
windowInterval: Double(windowMinutes) * 60)
}
private nonisolated static func normalizedBoundaryDate(_ date: Date) -> Date {
Date(timeIntervalSince1970: floor(date.timeIntervalSince1970))
}
private nonisolated static func effectivePeriodBoundaryDate(
for entry: PlanUtilizationHistoryEntry,
windowMinutes: Int,
rawResetBoundaryDate: Date?,
resetBoundaryLattice: ResetBoundaryLattice?) -> Date
{
if let rawResetBoundaryDate {
if let resetBoundaryLattice {
return self.closestPeriodBoundaryDate(
to: rawResetBoundaryDate,
resetBoundaryLattice: resetBoundaryLattice)
}
return rawResetBoundaryDate
}
if let resetBoundaryLattice {
return self.periodBoundaryDate(
containing: entry.capturedAt,
resetBoundaryLattice: resetBoundaryLattice)
}
return self.syntheticBoundaryDate(for: entry.capturedAt, windowMinutes: windowMinutes)
}
private nonisolated static func shouldPreferObservedPoint(
_ candidate: EntryPointAccumulator,
over existing: EntryPointAccumulator) -> Bool
{
if candidate.usedPercent != existing.usedPercent {
return candidate.usedPercent > existing.usedPercent
}
if candidate.hasObservedResetBoundary != existing.hasObservedResetBoundary {
return candidate.hasObservedResetBoundary
}
if candidate.displayBoundaryDate != existing.displayBoundaryDate {
return candidate.displayBoundaryDate > existing.displayBoundaryDate
}
return candidate.observedAt >= existing.observedAt
}
private nonisolated static func currentPeriodBoundaryDate(
for referenceDate: Date,
windowMinutes: Int,
resetBoundaryLattice: ResetBoundaryLattice?) -> Date
{
if let resetBoundaryLattice {
return self.periodBoundaryDate(
containing: referenceDate,
resetBoundaryLattice: resetBoundaryLattice)
}
return self.syntheticBoundaryDate(for: referenceDate, windowMinutes: windowMinutes)
}
private nonisolated static func closestPeriodBoundaryDate(
to rawBoundaryDate: Date,
resetBoundaryLattice: ResetBoundaryLattice) -> Date
{
let offset = rawBoundaryDate.timeIntervalSince(resetBoundaryLattice.referenceBoundaryDate)
let periodOffset = (offset / resetBoundaryLattice.windowInterval).rounded()
return resetBoundaryLattice.referenceBoundaryDate
.addingTimeInterval(periodOffset * resetBoundaryLattice.windowInterval)
}
private nonisolated static func periodBoundaryDate(
containing capturedAt: Date,
resetBoundaryLattice: ResetBoundaryLattice) -> Date
{
let offset = capturedAt.timeIntervalSince(resetBoundaryLattice.referenceBoundaryDate)
let periodOffset = ceil(offset / resetBoundaryLattice.windowInterval)
return resetBoundaryLattice.referenceBoundaryDate
.addingTimeInterval(periodOffset * resetBoundaryLattice.windowInterval)
}
private nonisolated static func syntheticBoundaryDate(for date: Date, windowMinutes: Int) -> Date {
let bucketSeconds = Double(windowMinutes) * 60
let bucketIndex = floor(date.timeIntervalSince1970 / bucketSeconds)
return Date(timeIntervalSince1970: (bucketIndex + 1) * bucketSeconds)
}
private nonisolated static func xDomain(points: [Point]) -> ClosedRange<Double>? {
guard !points.isEmpty else { return nil }
return -0.5...(Double(Layout.maxPoints) - 0.5)
}
private nonisolated static func axisIndexes(points: [Point], windowMinutes: Int) -> [Double] {
let candidateIndexes = self.axisCandidateIndexes(points: points, windowMinutes: windowMinutes)
return self.proportionalAxisIndexes(points: points, candidateIndexes: candidateIndexes)
}
private nonisolated static func axisCandidateIndexes(points: [Point], windowMinutes: Int) -> [Int] {
if windowMinutes <= 300 {
return self.sessionAxisCandidateIndexes(points: points)
}
return points.map(\.index)
}
private nonisolated static func sessionAxisCandidateIndexes(points: [Point]) -> [Int] {
guard let firstPoint = points.first else { return [] }
let calendar = Calendar.current
var previousPoint = firstPoint
var rawIndexes: [Int] = [firstPoint.index]
for point in points.dropFirst() {
if !calendar.isDate(point.date, inSameDayAs: previousPoint.date) {
rawIndexes.append(point.index)
}
previousPoint = point
}
return rawIndexes
}
private nonisolated static func proportionalAxisIndexes(points: [Point], candidateIndexes: [Int]) -> [Double] {
guard !points.isEmpty, !candidateIndexes.isEmpty else { return [] }
let occupiedFraction = Double(points.count) / Double(Layout.maxPoints)
let proportionalBudget = Int(ceil(Double(Layout.maxAxisLabels) * occupiedFraction))
let labelBudget = max(1, min(Layout.maxAxisLabels, proportionalBudget, candidateIndexes.count))
if labelBudget == 1 {
return [Double(candidateIndexes[0])]
}
let step = Double(candidateIndexes.count - 1) / Double(labelBudget - 1)
var selectedIndexes = (0..<labelBudget).map { position in
let candidateOffset = Int((Double(position) * step).rounded())
return candidateIndexes[candidateOffset]
}
selectedIndexes = Array(NSOrderedSet(array: selectedIndexes)) as? [Int] ?? selectedIndexes
let trailingLabelCutoff = points.first!.index + Int(floor(Double(points.count) * 0.8))
if selectedIndexes.count > 1,
let lastSelectedIndex = selectedIndexes.last,
lastSelectedIndex >= trailingLabelCutoff
{
selectedIndexes.removeLast()
}
if points.count == Layout.maxPoints,
let lastVisibleIndex = points.last?.index,
!selectedIndexes.contains(lastVisibleIndex)
{
selectedIndexes.append(lastVisibleIndex)
}
let deduplicated = Array(NSOrderedSet(array: selectedIndexes)) as? [Int] ?? selectedIndexes
return deduplicated.map(Double.init)
}
@ViewBuilder
private static func axisLabel(
for point: Point,
windowMinutes: Int,
isTrailingFullChartLabel: Bool) -> some View
{
let label = Text(point.date.formatted(self.axisFormat(windowMinutes: windowMinutes)))
.font(.caption2)
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
if isTrailingFullChartLabel {
label
.frame(width: 48, alignment: .trailing)
.offset(x: -24)
} else {
label
}
}
private nonisolated static func axisFormat(windowMinutes: Int) -> Date.FormatStyle {
if windowMinutes <= 300 {
return .dateTime.month(.abbreviated).day()
}
return .dateTime.month(.abbreviated).day()
}
private nonisolated static func seriesTitle(
name: PlanUtilizationSeriesName,
metadata: ProviderMetadata?) -> String
{
switch name {
case .session:
L(metadata?.sessionLabel ?? "Session")
case .weekly:
L(metadata?.weeklyLabel ?? "Weekly")
case .opus:
metadata?.opusLabel ?? "Opus"
default:
self.fallbackTitle(for: name.rawValue)
}
}
private nonisolated static func fallbackTitle(for rawValue: String) -> String {
let words = rawValue
.replacingOccurrences(of: "([a-z0-9])([A-Z])", with: "$1 $2", options: .regularExpression)
.split(separator: " ")
return words.map { $0.prefix(1).uppercased() + $0.dropFirst() }.joined(separator: " ")
}
private nonisolated static func seriesSortOrder(_ name: PlanUtilizationSeriesName) -> Int {
switch name {
case .session:
0
case .weekly:
1
case .opus:
2
default:
100
}
}
private nonisolated static func emptyStateText(title: String?) -> String {
if let title {
return String(format: L("No %@ utilization data yet."), title.lowercased())
}
return L("No utilization data yet.")
}
#if DEBUG
struct ModelSnapshot: Equatable {
let pointCount: Int
let axisIndexes: [Double]
let xDomain: ClosedRange<Double>?
let selectedSeries: String?
let visibleSeries: [String]
let usedPercents: [Double]
let pointDates: [String]
}
nonisolated static func _modelSnapshotForTesting(
selectedSeriesRawValue: String? = nil,
histories: [PlanUtilizationSeriesHistory],
provider: UsageProvider,
snapshot: UsageSnapshot? = nil,
referenceDate: Date? = nil) -> ModelSnapshot
{
let visibleSeries = self.visibleSeries(histories: histories, provider: provider, snapshot: snapshot)
let selectedSeries = visibleSeries.first(where: { $0.id == selectedSeriesRawValue }) ?? visibleSeries.first
let model = self.makeModel(
history: selectedSeries?.history,
provider: provider,
referenceDate: referenceDate ?? histories.flatMap(\.entries).map(\.capturedAt).max() ?? Date())
return ModelSnapshot(
pointCount: model.points.count,
axisIndexes: model.axisIndexes,
xDomain: model.xDomain,
selectedSeries: selectedSeries?.id,
visibleSeries: visibleSeries.map(\.id),
usedPercents: model.points.map(\.usedPercent),
pointDates: model.points.map { point in
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone.current
formatter.dateFormat = "yyyy-MM-dd HH:mm"
return formatter.string(from: point.date)
})
}
nonisolated static func _detailLineForTesting(
selectedSeriesRawValue: String? = nil,
histories: [PlanUtilizationSeriesHistory],
provider: UsageProvider,
snapshot: UsageSnapshot? = nil,
referenceDate: Date? = nil) -> String
{
let visibleSeries = self.visibleSeries(histories: histories, provider: provider, snapshot: snapshot)
let selectedSeries = visibleSeries.first(where: { $0.id == selectedSeriesRawValue }) ?? visibleSeries.first
let model = self.makeModel(
history: selectedSeries?.history,
provider: provider,
referenceDate: referenceDate ?? histories.flatMap(\.entries).map(\.capturedAt).max() ?? Date())
return self.detailLine(point: model.points.last, windowMinutes: selectedSeries?.history.windowMinutes ?? 0)
}
nonisolated static func _emptyStateTextForTesting(title: String?) -> String {
self.emptyStateText(title: title)
}
#endif
private func xValue(for index: Int) -> PlottableValue<Double> {
.value(L("Series"), Double(index))
}
@ViewBuilder
private func utilizationChart(model: Model) -> some View {
if let xDomain = model.xDomain {
Chart {
self.utilizationChartContent(model: model)
}
.chartXScale(domain: xDomain)
} else {
Chart {
self.utilizationChartContent(model: model)
}
}
}
@ChartContentBuilder
private func utilizationChartContent(model: Model) -> some ChartContent {
ForEach(model.points) { point in
BarMark(
x: self.xValue(for: point.index),
yStart: .value(L("Capacity Start"), 0),
yEnd: .value(L("Capacity End"), 100),
width: .fixed(Layout.barWidth))
.foregroundStyle(model.trackColor)
BarMark(
x: self.xValue(for: point.index),
yStart: .value(L("Utilization Start"), 0),
yEnd: .value(L("Utilization End"), point.usedPercent),
width: .fixed(Layout.barWidth))
.foregroundStyle(model.barColor)
}
if let selected = self.selectedPoint(model: model) {
RuleMark(x: self.xValue(for: selected.index))
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
.lineStyle(StrokeStyle(lineWidth: 1, dash: [3, 3]))
}
}
private func selectedPoint(model: Model) -> Point? {
guard let selectedPointID else { return nil }
return model.pointsByID[selectedPointID]
}
private func detailLine(model: Model, windowMinutes: Int) -> String {
let activePoint = self.selectedPoint(model: model) ?? model.points.last
return Self.detailLine(point: activePoint, windowMinutes: windowMinutes)
}
private func updateSelection(
location: CGPoint?,
model: Model,
proxy: ChartProxy,
geo: GeometryProxy)
{
guard let location else {
if self.selectedPointID != nil { self.selectedPointID = nil }
return
}
guard let plotAnchor = proxy.plotFrame else { return }
let plotFrame = geo[plotAnchor]
guard plotFrame.contains(location) else {
if self.selectedPointID != nil { self.selectedPointID = nil }
return
}
let xInPlot = location.x - plotFrame.origin.x
guard let xValue: Double = proxy.value(atX: xInPlot) else { return }
var best: (id: Date, distance: Double)?
for point in model.points {
let distance = abs(Double(point.index) - xValue)
if let current = best {
if distance < current.distance {
best = (point.id, distance)
}
} else {
best = (point.id, distance)
}
}
// Stay on the last selected bar when cursor is in the gap between bars; only switch
// selection when the cursor is over the bar's own visual body.
if let best, let bestPoint = model.pointsByID[best.id],
let barX = proxy.position(forX: Double(bestPoint.index))
{
guard ChartBarHoverSelection.accepts(
distanceFromBarCenter: abs(location.x - (plotFrame.origin.x + barX)),
barHalfWidth: Layout.barWidth / 2,
selectableCount: model.points.count)
else { return }
}
if self.selectedPointID != best?.id {
self.selectedPointID = best?.id
}
}
}
extension PlanUtilizationHistoryChartMenuView {
private nonisolated static func detailLine(point: Point?, windowMinutes: Int) -> String {
guard let point else {
return "-"
}
let dateLabel = self.detailDateLabel(for: point.date, windowMinutes: windowMinutes)
let used = max(0, min(100, point.usedPercent))
if !point.isObserved {
return "\(dateLabel): -"
}
let usedText = used.formatted(.number.precision(.fractionLength(0...1)))
return L("%@: %@%% used", dateLabel, usedText)
}
private nonisolated static func detailDateLabel(for date: Date, windowMinutes: Int) -> String {
let formatter = DateFormatter()
formatter.locale = codexBarLocalizedLocale()
formatter.timeZone = TimeZone.current
formatter.setLocalizedDateFormatFromTemplate("MMM d, h:mm a")
var rendered = formatter.string(from: date).replacingOccurrences(of: "\u{202F}", with: " ")
let amSymbol = formatter.amSymbol ?? ""
let pmSymbol = formatter.pmSymbol ?? ""
if !amSymbol.isEmpty {
rendered = rendered.replacingOccurrences(of: amSymbol, with: amSymbol.lowercased())
}
if !pmSymbol.isEmpty {
rendered = rendered.replacingOccurrences(of: pmSymbol, with: pmSymbol.lowercased())
}
return rendered
}
}
@@ -0,0 +1,361 @@
import CodexBarCore
import Foundation
struct PlanUtilizationSeriesName: RawRepresentable, Hashable, Codable, ExpressibleByStringLiteral, Sendable {
let rawValue: String
init(rawValue: String) {
self.rawValue = rawValue
}
init(stringLiteral value: StringLiteralType) {
self.rawValue = value
}
static let session: Self = "session"
static let weekly: Self = "weekly"
static let opus: Self = "opus"
func canonicalWindowMinutes(_ windowMinutes: Int) -> Int {
switch self {
case .session where (295...305).contains(windowMinutes):
300
case .weekly where (10070...10090).contains(windowMinutes):
10080
default:
windowMinutes
}
}
}
struct PlanUtilizationHistoryEntry: Codable, Equatable, Hashable, Sendable {
let capturedAt: Date
let usedPercent: Double
let resetsAt: Date?
}
struct PlanUtilizationSeriesHistory: Codable, Equatable, Sendable {
let name: PlanUtilizationSeriesName
let windowMinutes: Int
let entries: [PlanUtilizationHistoryEntry]
init(name: PlanUtilizationSeriesName, windowMinutes: Int, entries: [PlanUtilizationHistoryEntry]) {
self.name = name
self.windowMinutes = windowMinutes
self.entries = entries.sorted { lhs, rhs in
if lhs.capturedAt != rhs.capturedAt {
return lhs.capturedAt < rhs.capturedAt
}
if lhs.usedPercent != rhs.usedPercent {
return lhs.usedPercent < rhs.usedPercent
}
let lhsReset = lhs.resetsAt?.timeIntervalSince1970 ?? Date.distantPast.timeIntervalSince1970
let rhsReset = rhs.resetsAt?.timeIntervalSince1970 ?? Date.distantPast.timeIntervalSince1970
return lhsReset < rhsReset
}
}
var latestCapturedAt: Date? {
self.entries.last?.capturedAt
}
}
struct PlanUtilizationHistoryBuckets: Equatable, Sendable {
var preferredAccountKey: String?
var unscoped: [PlanUtilizationSeriesHistory] = []
var accounts: [String: [PlanUtilizationSeriesHistory]] = [:]
func histories(for accountKey: String?) -> [PlanUtilizationSeriesHistory] {
guard let accountKey, !accountKey.isEmpty else { return self.unscoped }
return self.accounts[accountKey] ?? []
}
mutating func setHistories(_ histories: [PlanUtilizationSeriesHistory], for accountKey: String?) {
let sorted = Self.sortedHistories(histories)
guard let accountKey, !accountKey.isEmpty else {
self.unscoped = sorted
return
}
if sorted.isEmpty {
self.accounts.removeValue(forKey: accountKey)
} else {
self.accounts[accountKey] = sorted
}
}
var isEmpty: Bool {
self.unscoped.isEmpty && self.accounts.values.allSatisfy(\.isEmpty)
}
private static func sortedHistories(_ histories: [PlanUtilizationSeriesHistory]) -> [PlanUtilizationSeriesHistory] {
histories.sorted { lhs, rhs in
if lhs.windowMinutes != rhs.windowMinutes {
return lhs.windowMinutes < rhs.windowMinutes
}
return lhs.name.rawValue < rhs.name.rawValue
}
}
}
private struct ProviderHistoryFile: Codable, Sendable {
let preferredAccountKey: String?
let unscoped: [PlanUtilizationSeriesHistory]
let accounts: [String: [PlanUtilizationSeriesHistory]]
}
private struct ProviderHistoryDocument: Codable, Sendable {
let version: Int
let preferredAccountKey: String?
let unscoped: [PlanUtilizationSeriesHistory]
let accounts: [String: [PlanUtilizationSeriesHistory]]
}
struct PlanUtilizationHistoryStore: Sendable {
fileprivate static let providerSchemaVersion = 1
let directoryURL: URL?
init(directoryURL: URL? = Self.defaultDirectoryURL()) {
self.directoryURL = directoryURL
}
static func defaultAppSupport() -> Self {
Self()
}
func load() -> [UsageProvider: PlanUtilizationHistoryBuckets] {
self.loadProviderFiles()
}
/// Loads the persisted histories on a utility-priority detached task.
///
/// The on-disk decode is synchronous I/O + JSON parsing that can take
/// ~150 ms for mature two-year histories and must not run on the app
/// startup main thread. The returned dictionary is safe to apply on the
/// main actor once decoding completes.
func loadAsync() async -> [UsageProvider: PlanUtilizationHistoryBuckets] {
await Task.detached(priority: .utility) { self.load() }.value
}
func save(_ providers: [UsageProvider: PlanUtilizationHistoryBuckets]) {
guard let directoryURL = self.directoryURL else { return }
do {
try FileManager.default.createDirectory(
at: directoryURL,
withIntermediateDirectories: true)
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = [.sortedKeys]
for provider in UsageProvider.allCases {
let fileURL = self.providerFileURL(for: provider)
let buckets = providers[provider] ?? PlanUtilizationHistoryBuckets()
let unscoped = Self.sortedHistories(buckets.unscoped)
let accounts = Self.sortedAccounts(buckets.accounts)
guard !unscoped.isEmpty || !accounts.isEmpty else {
try? FileManager.default.removeItem(at: fileURL)
continue
}
let payload = ProviderHistoryDocument(
version: Self.providerSchemaVersion,
preferredAccountKey: buckets.preferredAccountKey,
unscoped: unscoped,
accounts: accounts)
let data = try encoder.encode(payload)
try data.write(to: fileURL, options: Data.WritingOptions.atomic)
}
} catch {
// Best-effort persistence only.
}
}
private func loadProviderFiles() -> [UsageProvider: PlanUtilizationHistoryBuckets] {
guard self.directoryURL != nil else { return [:] }
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
var output: [UsageProvider: PlanUtilizationHistoryBuckets] = [:]
for provider in UsageProvider.allCases {
let fileURL = self.providerFileURL(for: provider)
guard FileManager.default.fileExists(atPath: fileURL.path) else { continue }
guard let data = try? Data(contentsOf: fileURL),
let decoded = try? decoder.decode(ProviderHistoryDocument.self, from: data)
else {
continue
}
let history = ProviderHistoryFile(
preferredAccountKey: decoded.preferredAccountKey,
unscoped: decoded.unscoped,
accounts: decoded.accounts)
output[provider] = Self.decodeProvider(history)
}
return output
}
private static func decodeProviders(
_ providers: [String: ProviderHistoryFile]) -> [UsageProvider: PlanUtilizationHistoryBuckets]
{
var output: [UsageProvider: PlanUtilizationHistoryBuckets] = [:]
for (rawProvider, providerHistory) in providers {
guard let provider = UsageProvider(rawValue: rawProvider) else { continue }
output[provider] = Self.decodeProvider(providerHistory)
}
return output
}
private static func decodeProvider(_ providerHistory: ProviderHistoryFile) -> PlanUtilizationHistoryBuckets {
PlanUtilizationHistoryBuckets(
preferredAccountKey: providerHistory.preferredAccountKey,
unscoped: self.sortedHistories(providerHistory.unscoped),
accounts: Dictionary(
uniqueKeysWithValues: providerHistory.accounts.compactMap { accountKey, histories in
let sorted = Self.sortedHistories(histories)
guard !sorted.isEmpty else { return nil }
return (accountKey, sorted)
}))
}
private static func sortedAccounts(
_ accounts: [String: [PlanUtilizationSeriesHistory]]) -> [String: [PlanUtilizationSeriesHistory]]
{
Dictionary(
uniqueKeysWithValues: accounts.compactMap { accountKey, histories in
let sorted = Self.sortedHistories(histories)
guard !sorted.isEmpty else { return nil }
return (accountKey, sorted)
})
}
private static func sortedHistories(_ histories: [PlanUtilizationSeriesHistory]) -> [PlanUtilizationSeriesHistory] {
self.sanitizedHistories(histories).sorted { lhs, rhs in
if lhs.windowMinutes != rhs.windowMinutes {
return lhs.windowMinutes < rhs.windowMinutes
}
return lhs.name.rawValue < rhs.name.rawValue
}
}
private static func sanitizedHistories(_ histories: [PlanUtilizationSeriesHistory])
-> [PlanUtilizationSeriesHistory] {
histories.filter { history in
history.windowMinutes > 0 && !history.entries.isEmpty
}
}
private static func defaultDirectoryURL() -> URL? {
guard let root = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
return nil
}
let dir = root.appendingPathComponent("com.steipete.codexbar", isDirectory: true)
return dir.appendingPathComponent("history", isDirectory: true)
}
private func providerFileURL(for provider: UsageProvider) -> URL {
let directoryURL = self.directoryURL ?? URL(fileURLWithPath: "/dev/null", isDirectory: true)
return directoryURL.appendingPathComponent("\(provider.rawValue).json", isDirectory: false)
}
}
extension ProviderHistoryDocument {
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let version = try container.decode(Int.self, forKey: .version)
guard version == PlanUtilizationHistoryStore.providerSchemaVersion else {
throw DecodingError.dataCorruptedError(
forKey: .version,
in: container,
debugDescription: "Unsupported provider history schema version \(version)")
}
self.version = version
self.preferredAccountKey = try container.decodeIfPresent(String.self, forKey: .preferredAccountKey)
self.unscoped = try container.decode([PlanUtilizationSeriesHistory].self, forKey: .unscoped)
self.accounts = try container.decode([String: [PlanUtilizationSeriesHistory]].self, forKey: .accounts)
}
}
/// One-shot synchronization primitive used by `UsageStore.init` to defer the
/// utility-priority plan-utilization history load until a test chooses to
/// release it. The default `nil` gate is open and the load proceeds immediately.
///
/// Used to verify that `UsageStore.init` returns before disk I/O completes and
/// that the history is applied exactly once after the gate opens.
final class PlanUtilizationHistoryLoadGate: @unchecked Sendable {
private enum State {
case closed
case open
case cancelled
}
private let lock = NSLock()
private var continuations: [CheckedContinuation<Bool, Never>] = []
private var state: State = .closed
init() {}
var isOpen: Bool {
self.lock.lock()
defer { self.lock.unlock() }
return self.state == .open
}
var isCancelled: Bool {
self.lock.lock()
defer { self.lock.unlock() }
return self.state == .cancelled
}
func wait() async -> Bool {
await withCheckedContinuation { continuation in
self.lock.lock()
switch self.state {
case .open:
self.lock.unlock()
continuation.resume(returning: true)
case .cancelled:
self.lock.unlock()
continuation.resume(returning: false)
case .closed:
self.continuations.append(continuation)
self.lock.unlock()
}
}
}
func open() {
self.lock.lock()
guard self.state == .closed else {
self.lock.unlock()
return
}
self.state = .open
let pending = self.continuations
self.continuations.removeAll()
self.lock.unlock()
for continuation in pending {
continuation.resume(returning: true)
}
}
/// Cancels this one-shot gate and resumes pending or future waiters with
/// `false`. Cancellation is sticky so it cannot race ahead of `wait()` and
/// lose the wakeup that drains the load task.
func cancel() {
self.lock.lock()
guard self.state == .closed else {
self.lock.unlock()
return
}
self.state = .cancelled
let pending = self.continuations
self.continuations.removeAll()
self.lock.unlock()
for continuation in pending {
continuation.resume(returning: false)
}
}
}
@@ -0,0 +1,284 @@
import CodexBarCore
import Foundation
struct PredictivePaceWarningStateKey: Hashable {
let provider: UsageProvider
let accountDiscriminator: String
let window: QuotaWarningWindow
let resetWindow: PredictivePaceWarningResetWindow
}
struct PredictivePaceWarningResetWindow: Hashable {
let windowMinutes: Int?
let resetsAt: Date
func belongsToSameCycle(as other: Self) -> Bool {
guard self.windowMinutes == other.windowMinutes else { return false }
let tolerance = self.windowMinutes.map { max(TimeInterval($0 * 60) / 2, 300) } ?? 300
return abs(self.resetsAt.timeIntervalSince(other.resetsAt)) < tolerance
}
}
struct PredictivePaceWarningEvent: Equatable {
let window: QuotaWarningWindow
let etaSeconds: TimeInterval
let accountDisplayName: String?
}
enum PredictivePaceWarningNotificationLogic {
static func notificationIDPrefix(provider: UsageProvider, event: PredictivePaceWarningEvent) -> String {
"predictive-pace-warning-\(provider.rawValue)-\(event.window.rawValue)"
}
static func notificationCopy(
providerName: String,
event: PredictivePaceWarningEvent,
now: Date = .init()) -> (title: String, body: String)
{
let windowLabel = event.window.localizedNotificationDisplayName
let title = L("predictive_pace_warning_notification_title", providerName, windowLabel)
let durationText = Self.durationText(seconds: event.etaSeconds, now: now)
let body = if let accountDisplayName = event.accountDisplayName {
L("predictive_pace_warning_notification_body_with_account", accountDisplayName, durationText)
} else {
L("predictive_pace_warning_notification_body", durationText)
}
return (title, body)
}
static func shouldNotify(pace: UsagePace) -> Bool {
guard !pace.willLastToReset else { return false }
guard let etaSeconds = pace.etaSeconds, etaSeconds > 0 else { return false }
guard (pace.runOutProbability ?? 1) >= 0.5 else { return false }
return true
}
static func recordObservation(
key: PredictivePaceWarningStateKey,
pace: UsagePace,
notifiedKeys: inout Set<PredictivePaceWarningStateKey>) -> Bool
{
if pace.willLastToReset {
notifiedKeys.remove(key)
return false
}
guard self.shouldNotify(pace: pace) else { return false }
guard !notifiedKeys.contains(key) else { return false }
notifiedKeys.insert(key)
return true
}
static func reconcileSiblingWindowKeys(
activeKey: PredictivePaceWarningStateKey,
notifiedKeys: inout Set<PredictivePaceWarningStateKey>)
{
let siblingKeys = notifiedKeys.filter { key in
key.provider == activeKey.provider &&
key.accountDiscriminator == activeKey.accountDiscriminator &&
key.window == activeKey.window
}
guard !siblingKeys.isEmpty else { return }
let alreadyWarnedThisCycle = siblingKeys.contains { key in
key.resetWindow.belongsToSameCycle(as: activeKey.resetWindow)
}
notifiedKeys.subtract(siblingKeys)
if alreadyWarnedThisCycle {
// Follow small provider reset-time corrections without re-alerting. Replacing the key
// lets successive relative-TTL observations move together instead of accumulating drift.
notifiedKeys.insert(activeKey)
}
}
private static func durationText(seconds: TimeInterval, now: Date) -> String {
let countdown = UsageFormatter.resetCountdownDescription(from: now.addingTimeInterval(seconds), now: now)
if countdown.hasPrefix("in ") {
return String(countdown.dropFirst(3))
}
return countdown
}
}
@MainActor
extension UsageStore {
func handlePredictivePaceWarningTransitions(
provider: UsageProvider,
snapshot: UsageSnapshot,
accountDiscriminatorOverride: String? = nil)
{
guard self.settings.predictivePaceWarningNotificationsEnabled else {
self.predictivePaceWarningNotifiedKeys = Set(
self.predictivePaceWarningNotifiedKeys.filter { $0.provider != provider })
return
}
guard provider == .codex || provider == .claude else { return }
guard let accountDiscriminator = self.predictivePaceWarningAccountDiscriminator(
provider: provider,
snapshot: snapshot,
accountDiscriminatorOverride: accountDiscriminatorOverride)
else { return }
let candidates = self.predictivePaceWarningCandidates(provider: provider, snapshot: snapshot)
for candidate in candidates {
guard let resetWindow = Self.predictivePaceWarningResetWindow(for: candidate.rateWindow) else {
continue
}
let key = PredictivePaceWarningStateKey(
provider: provider,
accountDiscriminator: accountDiscriminator,
window: candidate.window,
resetWindow: resetWindow)
PredictivePaceWarningNotificationLogic.reconcileSiblingWindowKeys(
activeKey: key,
notifiedKeys: &self.predictivePaceWarningNotifiedKeys)
guard PredictivePaceWarningNotificationLogic.recordObservation(
key: key,
pace: candidate.pace,
notifiedKeys: &self.predictivePaceWarningNotifiedKeys)
else { continue }
self.postPredictivePaceWarning(
PredictivePaceWarningEvent(
window: candidate.window,
etaSeconds: candidate.pace.etaSeconds ?? 0,
accountDisplayName: self.predictivePaceWarningAccountDisplayName(
provider: provider,
snapshot: snapshot)),
provider: provider,
now: snapshot.updatedAt)
}
}
private func predictivePaceWarningCandidates(
provider: UsageProvider,
snapshot: UsageSnapshot) -> [(window: QuotaWarningWindow, rateWindow: RateWindow, pace: UsagePace)]
{
var candidates: [(window: QuotaWarningWindow, rateWindow: RateWindow, pace: UsagePace)] = []
let now = snapshot.updatedAt
if let sessionWindow = self.predictivePaceWarningSessionWindow(provider: provider, snapshot: snapshot),
!sessionWindow.isSyntheticPlaceholder,
let sessionPace = UsagePaceText.sessionPace(provider: provider, window: sessionWindow, now: now)
{
candidates.append((window: .session, rateWindow: sessionWindow, pace: sessionPace))
}
if let weeklyWindow = self.predictivePaceWarningWeeklyWindow(provider: provider, snapshot: snapshot),
let weeklyPace = self.weeklyPace(provider: provider, window: weeklyWindow, now: now)
{
candidates.append((window: .weekly, rateWindow: weeklyWindow, pace: weeklyPace))
}
return candidates
}
private func predictivePaceWarningSessionWindow(provider: UsageProvider, snapshot: UsageSnapshot) -> RateWindow? {
if provider == .codex {
return self.codexConsumerProjection(
surface: .liveCard,
snapshotOverride: snapshot,
now: snapshot.updatedAt)
.sourceRateWindow(for: .session)
}
return self.sessionQuotaWindow(provider: provider, snapshot: snapshot)?.window
}
private func predictivePaceWarningWeeklyWindow(provider: UsageProvider, snapshot: UsageSnapshot) -> RateWindow? {
if provider == .codex {
return self.codexConsumerProjection(
surface: .liveCard,
snapshotOverride: snapshot,
now: snapshot.updatedAt)
.sourceRateWindow(for: .weekly)
}
return snapshot.secondary
}
private func predictivePaceWarningAccountDiscriminator(
provider: UsageProvider,
snapshot: UsageSnapshot,
accountDiscriminatorOverride: String? = nil) -> String?
{
if provider == .codex {
return self.codexOwnershipContext(
preferredEmail: snapshot.accountEmail(for: .codex),
snapshot: snapshot)
.canonicalKey
}
if let accountDiscriminatorOverride = accountDiscriminatorOverride?
.trimmingCharacters(in: .whitespacesAndNewlines),
!accountDiscriminatorOverride.isEmpty
{
return accountDiscriminatorOverride
}
guard let account = snapshot.accountEmail(for: provider)?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased(),
!account.isEmpty
else { return nil }
return "email:\(account)"
}
static func predictivePaceWarningClaudeAccountDiscriminator(
strategyKind: ProviderFetchKind,
observation: ClaudeOAuthActiveAccountObservation,
oauthHistoryOwnerIdentifier: String? = nil) -> String?
{
switch strategyKind {
case .cli:
return self.predictivePaceWarningClaudeActiveAccountDiscriminator(observation: observation)
case .oauth:
if let activeAccount = self.predictivePaceWarningClaudeActiveAccountDiscriminator(
observation: observation)
{
return activeAccount
}
guard let owner = oauthHistoryOwnerIdentifier?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased(),
!owner.isEmpty
else { return nil }
// OAuth usage has no email. Keep a credential-scoped fallback so predictive warnings still work
// when Claude's active-account metadata is unavailable, without merging unrelated accounts.
return "claude-oauth-owner:\(owner)"
case .apiToken, .localProbe, .web, .webDashboard:
return nil
}
}
private static func predictivePaceWarningClaudeActiveAccountDiscriminator(
observation: ClaudeOAuthActiveAccountObservation) -> String?
{
guard case let .stable(identity) = observation,
let identity = identity?.trimmingCharacters(in: .whitespacesAndNewlines),
!identity.isEmpty
else { return nil }
return "claude-account:\(identity)"
}
static func predictivePaceWarningTokenAccountDiscriminator(_ account: ProviderTokenAccount?) -> String? {
guard let account else { return nil }
return "token-account:\(account.id.uuidString.lowercased())"
}
private func predictivePaceWarningAccountDisplayName(provider: UsageProvider, snapshot: UsageSnapshot) -> String? {
guard !self.settings.hidePersonalInfo else { return nil }
let account = snapshot.accountEmail(for: provider)?
.trimmingCharacters(in: .whitespacesAndNewlines)
guard let account, !account.isEmpty else { return nil }
return account
}
private static func predictivePaceWarningResetWindow(for window: RateWindow)
-> PredictivePaceWarningResetWindow?
{
guard let resetsAt = window.resetsAt else { return nil }
return PredictivePaceWarningResetWindow(
windowMinutes: window.windowMinutes,
resetsAt: resetsAt)
}
}
+181
View File
@@ -0,0 +1,181 @@
import AppKit
import SwiftUI
@MainActor
struct AboutPane: View {
let updater: UpdaterProviding
@State private var iconHover = false
@AppStorage("autoUpdateEnabled") private var autoUpdateEnabled: Bool = true
@AppStorage(UpdateChannel.userDefaultsKey)
private var updateChannelRaw: String = UpdateChannel.defaultChannel.rawValue
@State private var didLoadUpdaterState = false
private var versionString: String {
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
return build.map { "\(version) (\($0))" } ?? version
}
private var buildTimestamp: String? {
guard let raw = Bundle.main.object(forInfoDictionaryKey: "CodexBuildTimestamp") as? String else { return nil }
let parser = ISO8601DateFormatter()
parser.formatOptions = [.withInternetDateTime]
guard let date = parser.date(from: raw) else { return raw }
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
formatter.locale = .current
return formatter.string(from: date)
}
var body: some View {
Form {
Section {
self.hero
.frame(maxWidth: .infinity)
.listRowBackground(Color.clear)
}
if self.updater.isAvailable {
Section {
Toggle(L("check_updates_auto"), isOn: self.$autoUpdateEnabled)
Picker(selection: self.updateChannelBinding) {
ForEach(UpdateChannel.allCases) { channel in
Text(channel.displayName).tag(channel)
}
} label: {
SettingsRowLabel(L("update_channel"), subtitle: self.updateChannel.description)
}
LabeledContent(String(format: L("version_format"), self.versionString)) {
Button(L("check_for_updates")) { self.updater.checkForUpdates(nil) }
}
} header: {
Text(L("section_updates"))
}
} else {
Section {
Text(self.updater.unavailableReason ?? L("updates_unavailable"))
.foregroundStyle(.secondary)
}
}
Section {
AboutLinkRow(
icon: "chevron.left.slash.chevron.right",
title: L("link_github"),
url: "https://github.com/steipete/CodexBar")
AboutLinkRow(icon: "globe", title: L("link_website"), url: "https://steipete.me")
AboutLinkRow(icon: "bird", title: L("link_twitter"), url: "https://twitter.com/steipete")
AboutLinkRow(icon: "envelope", title: L("link_email"), url: "mailto:peter@steipete.me")
} header: {
Text(L("section_links"))
} footer: {
Text(L("copyright"))
.frame(maxWidth: .infinity)
.multilineTextAlignment(.center)
}
}
.formStyle(.grouped)
.toggleStyle(.switch)
.scrollContentBackground(.hidden)
.onAppear {
guard !self.didLoadUpdaterState else { return }
// Align Sparkle's flag with the persisted preference on first load.
self.updater.automaticallyChecksForUpdates = self.autoUpdateEnabled
self.updater.automaticallyDownloadsUpdates = self.autoUpdateEnabled
self.didLoadUpdaterState = true
}
.onChange(of: self.autoUpdateEnabled) { _, newValue in
self.updater.automaticallyChecksForUpdates = newValue
self.updater.automaticallyDownloadsUpdates = newValue
}
}
private var hero: some View {
VStack(spacing: 10) {
if let image = NSApplication.shared.applicationIconImage {
Button(action: self.openProjectHome) {
Image(nsImage: image)
.resizable()
.frame(width: 92, height: 92)
.cornerRadius(16)
.scaleEffect(self.iconHover ? 1.05 : 1.0)
.shadow(color: self.iconHover ? .accentColor.opacity(0.25) : .clear, radius: 6)
}
.buttonStyle(.plain)
.focusEffectDisabled()
.onHover { hovering in
withAnimation(.spring(response: 0.32, dampingFraction: 0.78)) {
self.iconHover = hovering
}
}
}
VStack(spacing: 2) {
Text("CodexBar")
.font(.title3).bold()
Text(String(format: L("version_format"), self.versionString))
.foregroundStyle(.secondary)
if let buildTimestamp {
Text(String(format: L("built_format"), buildTimestamp))
.font(.footnote)
.foregroundStyle(.secondary)
}
Text(L("about_tagline"))
.font(.footnote)
.foregroundStyle(.secondary)
}
}
.padding(.vertical, 6)
}
private var updateChannel: UpdateChannel {
UpdateChannel(rawValue: self.updateChannelRaw) ?? .stable
}
private var updateChannelBinding: Binding<UpdateChannel> {
Binding(
get: { self.updateChannel },
set: { newValue in
self.updateChannelRaw = newValue.rawValue
self.updater.checkForUpdates(nil)
})
}
private func openProjectHome() {
guard let url = URL(string: "https://github.com/steipete/CodexBar") else { return }
NSWorkspace.shared.open(url)
}
}
@MainActor
struct AboutLinkRow: View {
let icon: String
let title: String
let url: String
@State private var hovering = false
var body: some View {
Button {
if let url = URL(string: self.url) { NSWorkspace.shared.open(url) }
} label: {
HStack(spacing: 8) {
Image(systemName: self.icon)
.frame(width: 18)
.foregroundStyle(.secondary)
Text(self.title)
.foregroundStyle(.primary)
Spacer()
Image(systemName: "arrow.up.right")
.font(.caption)
.foregroundStyle(self.hovering ? Color.accentColor : Color.secondary)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.onHover { self.hovering = $0 }
}
}
@@ -0,0 +1,129 @@
import CodexBarCore
import SwiftUI
@MainActor
struct AdvancedPane: View {
@Bindable var settings: SettingsStore
@Bindable var store: UsageStore
@State private var isInstallingCLI = false
@State private var cliStatus: String?
var body: some View {
Form {
Section {
LabeledContent {
Button {
Task { await self.installCLI() }
} label: {
if self.isInstallingCLI {
ProgressView().controlSize(.small)
} else {
Text(L("install_cli"))
}
}
.disabled(self.isInstallingCLI)
} label: {
SettingsRowLabel(L("install_cli"), subtitle: L("install_cli_subtitle"))
}
} header: {
Text(L("section_command_line"))
} footer: {
if let status = self.cliStatus {
SettingsSectionFooter(status)
}
}
Section {
Toggle(isOn: self.$settings.hidePersonalInfo) {
SettingsRowLabel(L("hide_personal_info_title"), subtitle: L("hide_personal_info_subtitle"))
}
Toggle(isOn: self.$settings.debugDisableKeychainAccess) {
SettingsRowLabel(
L("disable_keychain_access_title"),
subtitle: L("disable_keychain_access_subtitle"))
}
} header: {
Text(L("section_privacy"))
} footer: {
SettingsSectionFooter(L("keychain_access_caption"))
}
Section {
Toggle(isOn: self.$settings.providerStorageFootprintsEnabled) {
SettingsRowLabel(
L("show_provider_storage_usage_title"),
subtitle: L("show_provider_storage_usage_subtitle"))
}
Toggle(isOn: self.$settings.debugMenuEnabled) {
SettingsRowLabel(L("show_debug_settings_title"), subtitle: L("show_debug_settings_subtitle"))
}
} header: {
Text(L("section_diagnostics"))
}
}
.formStyle(.grouped)
.toggleStyle(.switch)
.scrollContentBackground(.hidden)
}
}
extension AdvancedPane {
private func installCLI() async {
if self.isInstallingCLI { return }
self.isInstallingCLI = true
defer { self.isInstallingCLI = false }
let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Helpers/CodexBarCLI")
let fm = FileManager.default
guard fm.fileExists(atPath: helperURL.path) else {
self.cliStatus = L("cli_not_found")
return
}
let destinations = [
"/usr/local/bin/codexbar",
"/opt/homebrew/bin/codexbar",
]
var results: [String] = []
for dest in destinations {
let dir = (dest as NSString).deletingLastPathComponent
guard fm.fileExists(atPath: dir) else { continue }
guard fm.isWritableFile(atPath: dir) else {
results.append("No write access: \(dir)")
continue
}
if fm.fileExists(atPath: dest) {
if Self.isLink(atPath: dest, pointingTo: helperURL.path) {
results.append("Installed: \(dir)")
} else {
results.append("Exists: \(dir)")
}
continue
}
do {
try fm.createSymbolicLink(atPath: dest, withDestinationPath: helperURL.path)
results.append("Installed: \(dir)")
} catch {
results.append("Failed: \(dir)")
}
}
self.cliStatus = results.isEmpty
? L("no_writable_bin_dirs")
: results.joined(separator: " · ")
}
private static func isLink(atPath path: String, pointingTo destination: String) -> Bool {
guard let link = try? FileManager.default.destinationOfSymbolicLink(atPath: path) else { return false }
let dir = (path as NSString).deletingLastPathComponent
let resolved = URL(fileURLWithPath: link, relativeTo: URL(fileURLWithPath: dir))
.standardizedFileURL
.path
return resolved == destination
}
}
@@ -0,0 +1,333 @@
import Foundation
import SwiftUI
protocol CodexAmbientLoginRunning: Sendable {
func run(timeout: TimeInterval) async -> CodexLoginRunner.Result
}
struct DefaultCodexAmbientLoginRunner: CodexAmbientLoginRunning {
func run(timeout: TimeInterval) async -> CodexLoginRunner.Result {
await CodexLoginRunner.run(timeout: timeout)
}
}
struct CodexAccountsSectionNotice: Equatable {
enum Tone: Equatable {
case secondary
case warning
}
let text: String
let tone: Tone
}
struct CodexAccountsSectionState: Equatable {
let visibleAccounts: [CodexVisibleAccount]
let activeVisibleAccountID: String?
let liveVisibleAccountID: String?
let hasUnreadableManagedAccountStore: Bool
let isAuthenticatingManagedAccount: Bool
let authenticatingManagedAccountID: UUID?
let isRemovingManagedAccount: Bool
let isAuthenticatingLiveAccount: Bool
let isPromotingSystemAccount: Bool
let notice: CodexAccountsSectionNotice?
var showsActivePicker: Bool {
self.visibleAccounts.count > 1
}
var singleVisibleAccount: CodexVisibleAccount? {
self.visibleAccounts.count == 1 ? self.visibleAccounts.first : nil
}
var systemVisibleAccount: CodexVisibleAccount? {
guard let liveVisibleAccountID else { return nil }
return self.visibleAccounts.first { $0.id == liveVisibleAccountID }
}
var showsSystemPicker: Bool {
self.visibleAccounts.count > 1 || (self.liveVisibleAccountID == nil && !self.visibleAccounts.isEmpty)
}
var systemDisplayName: String {
self.systemVisibleAccount?.displayName ?? L("No system account")
}
var canAddAccount: Bool {
!self.hasUnreadableManagedAccountStore &&
!self.isAuthenticatingManagedAccount &&
!self.isRemovingManagedAccount &&
!self.isAuthenticatingLiveAccount &&
!self.isPromotingSystemAccount
}
var addAccountTitle: String {
if self.isAuthenticatingManagedAccount, self.authenticatingManagedAccountID == nil {
return L("Adding Account…")
}
return L("Add Account")
}
func showsLiveBadge(for account: CodexVisibleAccount) -> Bool {
account.isLive
}
var isSystemSelectionDisabled: Bool {
self.hasUnreadableManagedAccountStore ||
self.isAuthenticatingManagedAccount ||
self.isRemovingManagedAccount ||
self.isAuthenticatingLiveAccount ||
self.isPromotingSystemAccount
}
func canPromoteToSystem(_ account: CodexVisibleAccount) -> Bool {
guard self.isSystemSelectionDisabled == false else { return false }
guard account.id != self.liveVisibleAccountID else { return false }
return account.storedAccountID != nil
}
func canReauthenticate(_ account: CodexVisibleAccount) -> Bool {
guard account.canReauthenticate else { return false }
guard self.isAuthenticatingManagedAccount == false else { return false }
guard self.isRemovingManagedAccount == false else { return false }
guard self.isAuthenticatingLiveAccount == false else { return false }
guard self.isPromotingSystemAccount == false else { return false }
if account.storedAccountID != nil {
return self.hasUnreadableManagedAccountStore == false
}
return true
}
func canRemove(_ account: CodexVisibleAccount) -> Bool {
guard account.canRemove else { return false }
guard self.isAuthenticatingManagedAccount == false else { return false }
guard self.isRemovingManagedAccount == false else { return false }
guard self.isAuthenticatingLiveAccount == false else { return false }
guard self.isPromotingSystemAccount == false else { return false }
return self.hasUnreadableManagedAccountStore == false
}
func reauthenticateTitle(for account: CodexVisibleAccount) -> String {
if let accountID = account.storedAccountID,
self.isAuthenticatingManagedAccount,
self.authenticatingManagedAccountID == accountID
{
return L("Re-authenticating…")
}
if account.storedAccountID == nil, self.isAuthenticatingLiveAccount {
return L("Re-authenticating…")
}
return L("Re-auth")
}
}
@MainActor
struct CodexAccountsSectionView: View {
let state: CodexAccountsSectionState
let setActiveVisibleAccount: (String) -> Void
let reauthenticateAccount: (CodexVisibleAccount) -> Void
let removeAccount: (CodexVisibleAccount) -> Void
let requestSystemVisibleAccount: (String) -> Void
let addAccount: () -> Void
var body: some View {
Section {
if let selection = self.activeSelectionBinding {
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .firstTextBaseline, spacing: 10) {
Text(L("Active"))
.font(.subheadline.weight(.semibold))
.frame(width: ProviderSettingsMetrics.pickerLabelWidth, alignment: .leading)
Picker("", selection: selection) {
ForEach(self.state.visibleAccounts) { account in
Text(account.displayName).tag(account.id)
}
}
.labelsHidden()
.pickerStyle(.menu)
.controlSize(.small)
Spacer(minLength: 0)
}
Text(L("Choose which Codex account CodexBar should follow."))
.font(.footnote)
.foregroundStyle(.secondary)
self.systemRow(selection: self.systemSelectionBinding)
}
.disabled(
self.state.isAuthenticatingManagedAccount ||
self.state.isRemovingManagedAccount ||
self.state.isAuthenticatingLiveAccount ||
self.state.isPromotingSystemAccount)
} else if let account = self.state.singleVisibleAccount {
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .firstTextBaseline, spacing: 10) {
Text(L("Account"))
.font(.subheadline.weight(.semibold))
.frame(width: ProviderSettingsMetrics.pickerLabelWidth, alignment: .leading)
Text(account.displayName)
.font(.subheadline)
Spacer(minLength: 0)
}
self.systemRow(selection: nil)
}
}
if self.state.visibleAccounts.isEmpty {
Text(L("No Codex accounts detected yet."))
.font(.footnote)
.foregroundStyle(.secondary)
} else {
VStack(alignment: .leading, spacing: 10) {
ForEach(self.state.visibleAccounts) { account in
CodexAccountsSectionRowView(
account: account,
showsSystemBadge: self.state.showsLiveBadge(for: account),
reauthenticateTitle: self.state.reauthenticateTitle(for: account),
canReauthenticate: self.state.canReauthenticate(account),
canRemove: self.state.canRemove(account),
onReauthenticate: { self.reauthenticateAccount(account) },
onRemove: { self.removeAccount(account) })
}
}
}
if let notice = self.state.notice {
Text(notice.text)
.font(.footnote)
.foregroundStyle(notice.tone == .warning ? .red : .secondary)
.fixedSize(horizontal: false, vertical: true)
}
Button(self.state.addAccountTitle) {
self.addAccount()
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(self.state.canAddAccount == false)
} header: {
Text(L("Accounts"))
}
}
private var activeSelectionBinding: Binding<String>? {
guard self.state.showsActivePicker else { return nil }
let fallbackID = self.state.activeVisibleAccountID ?? self.state.visibleAccounts.first?.id
guard let fallbackID else { return nil }
return Binding(
get: { self.state.activeVisibleAccountID ?? fallbackID },
set: { self.setActiveVisibleAccount($0) })
}
private var systemSelectionBinding: Binding<String>? {
guard self.state.showsSystemPicker else { return nil }
guard let liveVisibleAccountID = self.state.liveVisibleAccountID else { return nil }
return Binding(
get: { self.state.liveVisibleAccountID ?? liveVisibleAccountID },
set: { self.requestSystemVisibleAccount($0) })
}
@ViewBuilder
private func systemRow(selection: Binding<String>?) -> some View {
HStack(alignment: .firstTextBaseline, spacing: 10) {
Text(L("System"))
.font(.subheadline.weight(.semibold))
.frame(width: ProviderSettingsMetrics.pickerLabelWidth, alignment: .leading)
if let selection {
Picker("", selection: selection) {
ForEach(self.state.visibleAccounts) { account in
Text(account.displayName)
.tag(account.id)
}
}
.labelsHidden()
.pickerStyle(.menu)
.controlSize(.small)
.disabled(self.state.isSystemSelectionDisabled)
} else if self.state.showsSystemPicker {
Menu {
ForEach(self.state.visibleAccounts) { account in
Button(account.displayName) {
self.requestSystemVisibleAccount(account.id)
}
.disabled(self.state.canPromoteToSystem(account) == false)
}
} label: {
Text(self.state.systemDisplayName)
.font(.subheadline)
.foregroundStyle(.secondary)
}
.disabled(self.state.isSystemSelectionDisabled)
} else {
Text(self.state.systemDisplayName)
.font(.subheadline)
.foregroundStyle(self.state.systemVisibleAccount == nil ? .secondary : .primary)
}
Spacer(minLength: 0)
}
Text(L("The default Codex account on this Mac."))
.font(.footnote)
.foregroundStyle(.secondary)
}
}
private struct CodexAccountsSectionRowView: View {
let account: CodexVisibleAccount
let showsSystemBadge: Bool
let reauthenticateTitle: String
let canReauthenticate: Bool
let canRemove: Bool
let onReauthenticate: () -> Void
let onRemove: () -> Void
var body: some View {
HStack(alignment: .center, spacing: 12) {
VStack(alignment: .leading, spacing: 2) {
HStack(alignment: .firstTextBaseline, spacing: 6) {
Text(self.account.displayName)
.font(.subheadline.weight(.semibold))
if self.showsSystemBadge {
Text(L("(System)"))
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
}
}
if let health = self.account.authenticationHealthLabel {
Text(health)
.font(.caption)
.foregroundStyle(.orange)
}
}
Spacer(minLength: 8)
if self.account.canReauthenticate {
Button(self.reauthenticateTitle) {
self.onReauthenticate()
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(self.canReauthenticate == false)
}
if self.account.canRemove {
Button(L("Remove")) {
self.onRemove()
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(self.canRemove == false)
}
}
}
}
@@ -0,0 +1,167 @@
import AppKit
import KeyboardShortcuts
import SwiftUI
/// Colored rounded-square symbol used for app panes in the settings sidebar,
/// mirroring the System Settings sidebar style.
struct SettingsIconChip: View {
static let side: CGFloat = 20
let systemImage: String
let color: Color
var body: some View {
Image(systemName: self.systemImage)
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(.white)
.frame(width: Self.side, height: Self.side)
.background(
RoundedRectangle(cornerRadius: 5, style: .continuous)
.fill(LinearGradient(
colors: [self.color.opacity(0.85), self.color],
startPoint: .top,
endPoint: .bottom)))
.accessibilityHidden(true)
}
}
/// Two-line label for grouped-form rows that genuinely need a supporting sentence.
struct SettingsRowLabel: View {
let title: String
let subtitle: String?
init(_ title: String, subtitle: String? = nil) {
self.title = title
self.subtitle = subtitle
}
var body: some View {
VStack(alignment: .leading, spacing: 2) {
Text(self.title)
if let subtitle, !subtitle.isEmpty {
Text(subtitle)
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
}
}
/// Section footer for grouped forms. macOS renders bare footer text trailing-aligned
/// at body size, which reads badly for long captions; this pins it leading at footnote
/// size in secondary color, matching System Settings captions.
struct SettingsSectionFooter<Content: View>: View {
private let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
self.content
.font(.footnote)
.foregroundStyle(.secondary)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
extension SettingsSectionFooter where Content == Text {
init(_ text: String) {
self.init { Text(text) }
}
}
@MainActor
struct OpenMenuShortcutRecorder: NSViewRepresentable {
static let preferredWidth: CGFloat = 170
func makeNSView(context: Context) -> KeyboardShortcuts.RecorderCocoa {
KeyboardShortcuts.RecorderCocoa(for: .openMenu)
}
func updateNSView(_ nsView: KeyboardShortcuts.RecorderCocoa, context: Context) {
nsView.shortcutName = .openMenu
}
func sizeThatFits(
_: ProposedViewSize,
nsView: KeyboardShortcuts.RecorderCocoa,
context: Context)
-> CGSize?
{
Self.fittedSize(intrinsicHeight: nsView.intrinsicContentSize.height)
}
static func fittedSize(intrinsicHeight: CGFloat) -> CGSize {
CGSize(width: self.preferredWidth, height: intrinsicHeight)
}
}
// MARK: - Legacy building blocks (Debug pane)
@MainActor
struct PreferenceToggleRow: View {
let title: String
let subtitle: String?
@Binding var binding: Bool
var body: some View {
VStack(alignment: .leading, spacing: 5.4) {
Toggle(isOn: self.$binding) {
Text(self.title)
.font(.body)
}
.toggleStyle(.checkbox)
if let subtitle, !subtitle.isEmpty {
Text(subtitle)
.font(.footnote)
.foregroundStyle(.tertiary)
.fixedSize(horizontal: false, vertical: true)
}
}
}
}
@MainActor
struct SettingsSection<Content: View>: View {
let title: String?
let caption: String?
let contentSpacing: CGFloat
private let content: () -> Content
init(
title: String? = nil,
caption: String? = nil,
contentSpacing: CGFloat = 14,
@ViewBuilder content: @escaping () -> Content)
{
self.title = title
self.caption = caption
self.contentSpacing = contentSpacing
self.content = content
}
var body: some View {
VStack(alignment: .leading, spacing: 10) {
if let title, !title.isEmpty {
Text(title)
.font(.subheadline.weight(.semibold))
}
if let caption {
Text(caption)
.font(.footnote)
.foregroundStyle(.tertiary)
.fixedSize(horizontal: false, vertical: true)
}
VStack(alignment: .leading, spacing: self.contentSpacing) {
self.content()
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
+563
View File
@@ -0,0 +1,563 @@
import AppKit
import CodexBarCore
import SwiftUI
@MainActor
struct DebugPane: View {
@Bindable var settings: SettingsStore
@Bindable var store: UsageStore
@AppStorage("debugFileLoggingEnabled") private var debugFileLoggingEnabled = false
@State private var currentLogProvider: UsageProvider = .codex
@State private var currentFetchProvider: UsageProvider = .codex
@State private var isLoadingLog = false
@State private var logText: String = ""
@State private var isClearingCostCache = false
@State private var costCacheStatus: String?
@State private var cookieCacheStatus: String?
#if DEBUG
@State private var currentErrorProvider: UsageProvider = .codex
@State private var simulatedErrorText: String = """
Simulated error for testing layout.
Second line.
Third line.
Fourth line.
"""
#endif
var body: some View {
ScrollView(.vertical, showsIndicators: true) {
VStack(alignment: .leading, spacing: 20) {
SettingsSection(title: L("section_logging")) {
PreferenceToggleRow(
title: L("enable_file_logging"),
subtitle: String(format: L("enable_file_logging_subtitle"), self.fileLogPath),
binding: self.$debugFileLoggingEnabled)
.onChange(of: self.debugFileLoggingEnabled) { _, newValue in
if self.settings.debugFileLoggingEnabled != newValue {
self.settings.debugFileLoggingEnabled = newValue
}
}
HStack(alignment: .center, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text(L("verbosity_title"))
.font(.body)
Text(L("verbosity_subtitle"))
.font(.footnote)
.foregroundStyle(.tertiary)
}
Spacer()
Picker(L("Verbosity"), selection: self.$settings.debugLogLevel) {
ForEach(CodexBarLog.Level.allCases) { level in
Text(level.displayName).tag(level)
}
}
.labelsHidden()
.pickerStyle(.menu)
.frame(maxWidth: 160)
}
Button {
NSWorkspace.shared.open(CodexBarLog.fileLogURL)
} label: {
Label(L("open_log_file"), systemImage: "doc.text.magnifyingglass")
}
.controlSize(.small)
}
SettingsSection {
PreferenceToggleRow(
title: L("force_animation_next_refresh"),
subtitle: L("force_animation_next_refresh_subtitle"),
binding: self.$store.debugForceAnimation)
}
SettingsSection(
title: L("section_loading_animations"),
caption: L("loading_animations_caption"))
{
Picker(L("Animation pattern"), selection: self.animationPatternBinding) {
Text(L("animation_random_default")).tag(nil as LoadingPattern?)
ForEach(LoadingPattern.allCases) { pattern in
Text(pattern.displayName).tag(Optional(pattern))
}
}
.pickerStyle(.radioGroup)
Button(L("replay_selected_animation")) {
self.replaySelectedAnimation()
}
.keyboardShortcut(.defaultAction)
Button {
NotificationCenter.default.post(name: .codexbarDebugBlinkNow, object: nil)
} label: {
Label(L("blink_now"), systemImage: "eyes")
}
.controlSize(.small)
}
SettingsSection(
title: L("section_probe_logs"),
caption: L("probe_logs_caption"))
{
Picker(L("Provider"), selection: self.$currentLogProvider) {
Text("Codex").tag(UsageProvider.codex)
Text("Claude").tag(UsageProvider.claude)
Text("Cursor").tag(UsageProvider.cursor)
Text("Augment").tag(UsageProvider.augment)
Text("Amp").tag(UsageProvider.amp)
Text("Ollama").tag(UsageProvider.ollama)
}
.pickerStyle(.segmented)
.frame(width: 460)
HStack(spacing: 12) {
Button { self.loadLog(self.currentLogProvider) } label: {
Label(L("fetch_log"), systemImage: "arrow.clockwise")
}
.disabled(self.isLoadingLog)
Button { self.copyToPasteboard(self.logText) } label: {
Label(L("copy"), systemImage: "doc.on.doc")
}
.disabled(self.logText.isEmpty)
Button { self.saveLog(self.currentLogProvider) } label: {
Label(L("save_to_file"), systemImage: "externaldrive.badge.plus")
}
.disabled(self.isLoadingLog && self.logText.isEmpty)
if self.currentLogProvider == .claude {
Button { self.loadClaudeDump() } label: {
Label(L("load_parse_dump"), systemImage: "doc.text.magnifyingglass")
}
.disabled(self.isLoadingLog)
}
}
Button {
self.settings.rerunProviderDetection()
self.loadLog(self.currentLogProvider)
} label: {
Label(L("rerun_provider_autodetect"), systemImage: "dot.radiowaves.left.and.right")
}
.controlSize(.small)
ZStack(alignment: .topLeading) {
ScrollView {
Text(self.displayedLog)
.font(.system(.footnote, design: .monospaced))
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(8)
}
.frame(minHeight: 160, maxHeight: 220)
.background(Color(NSColor.textBackgroundColor))
.cornerRadius(6)
if self.isLoadingLog {
ProgressView()
.progressViewStyle(.circular)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.padding()
}
}
}
SettingsSection(
title: L("section_fetch_strategy"),
caption: L("fetch_strategy_caption"))
{
Picker(L("Provider"), selection: self.$currentFetchProvider) {
ForEach(UsageProvider.allCases, id: \.self) { provider in
Text(provider.rawValue.capitalized).tag(provider)
}
}
.pickerStyle(.menu)
.frame(width: 240)
ScrollView {
Text(self.fetchAttemptsText(for: self.currentFetchProvider))
.font(.system(.footnote, design: .monospaced))
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(8)
}
.frame(minHeight: 120, maxHeight: 220)
.background(Color(NSColor.textBackgroundColor))
.cornerRadius(6)
}
if !self.settings.debugDisableKeychainAccess {
SettingsSection(
title: L("section_openai_cookies"),
caption: L("openai_cookies_caption"))
{
HStack(spacing: 12) {
Button {
self.copyToPasteboard(self.store.openAIDashboardCookieImportDebugLog ?? "")
} label: {
Label(L("copy"), systemImage: "doc.on.doc")
}
.disabled((self.store.openAIDashboardCookieImportDebugLog ?? "").isEmpty)
}
ScrollView {
Text(
self.store.openAIDashboardCookieImportDebugLog?.isEmpty == false
? (self.store.openAIDashboardCookieImportDebugLog ?? "")
: L("no_log_yet"))
.font(.system(.footnote, design: .monospaced))
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(8)
}
.frame(minHeight: 120, maxHeight: 180)
.background(Color(NSColor.textBackgroundColor))
.cornerRadius(6)
}
}
SettingsSection(
title: L("section_caches"),
caption: L("caches_caption"))
{
let isTokenRefreshActive = self.store.isTokenRefreshInFlight(for: .codex)
|| self.store.isTokenRefreshInFlight(for: .claude)
HStack(spacing: 12) {
Button {
Task { await self.clearCostCache() }
} label: {
Label(L("clear_cost_cache"), systemImage: "trash")
}
.disabled(self.isClearingCostCache || isTokenRefreshActive)
if let status = self.costCacheStatus {
Text(status)
.font(.footnote)
.foregroundStyle(.tertiary)
}
}
HStack(spacing: 12) {
Button {
self.clearCookieCache()
} label: {
Label(L("clear_cookie_cache"), systemImage: "trash")
}
if let status = self.cookieCacheStatus {
Text(status)
.font(.footnote)
.foregroundStyle(.tertiary)
}
}
}
SettingsSection(
title: L("section_notifications"),
caption: L("notifications_caption"))
{
Picker(L("Provider"), selection: self.$currentLogProvider) {
Text("Codex").tag(UsageProvider.codex)
Text("Claude").tag(UsageProvider.claude)
}
.pickerStyle(.segmented)
.frame(width: 240)
HStack(spacing: 12) {
Button {
self.postSessionNotification(.depleted, provider: self.currentLogProvider)
} label: {
Label(L("post_depleted"), systemImage: "bell.badge")
}
.controlSize(.small)
Button {
self.postSessionNotification(.restored, provider: self.currentLogProvider)
} label: {
Label(L("post_restored"), systemImage: "bell")
}
.controlSize(.small)
}
}
SettingsSection(
title: L("section_cli_sessions"),
caption: L("cli_sessions_caption"))
{
PreferenceToggleRow(
title: L("keep_cli_sessions_alive"),
subtitle: L("keep_cli_sessions_alive_subtitle"),
binding: self.$settings.debugKeepCLISessionsAlive)
Button {
Task {
await CLIProbeSessionResetter.resetAll()
}
} label: {
Label(L("reset_cli_sessions"), systemImage: "arrow.counterclockwise")
}
.controlSize(.small)
}
#if DEBUG
SettingsSection(
title: L("section_error_simulation"),
caption: L("error_simulation_caption"))
{
Picker(L("Provider"), selection: self.$currentErrorProvider) {
Text("Codex").tag(UsageProvider.codex)
Text("Claude").tag(UsageProvider.claude)
Text("Gemini").tag(UsageProvider.gemini)
Text("Antigravity").tag(UsageProvider.antigravity)
Text("Augment").tag(UsageProvider.augment)
Text("Amp").tag(UsageProvider.amp)
Text("T3 Chat").tag(UsageProvider.t3chat)
Text("Ollama").tag(UsageProvider.ollama)
}
.pickerStyle(.segmented)
.frame(width: 360)
TextField(L("Simulated error text"), text: self.$simulatedErrorText, axis: .vertical)
.lineLimit(4)
HStack(spacing: 12) {
Button {
self.store._setErrorForTesting(
self.simulatedErrorText,
provider: self.currentErrorProvider)
} label: {
Label(L("set_menu_error"), systemImage: "exclamationmark.triangle")
}
.controlSize(.small)
Button {
self.store._setErrorForTesting(nil, provider: self.currentErrorProvider)
} label: {
Label(L("clear_menu_error"), systemImage: "xmark.circle")
}
.controlSize(.small)
}
let supportsTokenError = self.currentErrorProvider == .codex || self.currentErrorProvider == .claude
HStack(spacing: 12) {
Button {
self.store._setTokenErrorForTesting(
self.simulatedErrorText,
provider: self.currentErrorProvider)
} label: {
Label(L("set_cost_error"), systemImage: "banknote")
}
.controlSize(.small)
.disabled(!supportsTokenError)
Button {
self.store._setTokenErrorForTesting(nil, provider: self.currentErrorProvider)
} label: {
Label(L("clear_cost_error"), systemImage: "xmark.circle")
}
.controlSize(.small)
.disabled(!supportsTokenError)
}
}
#endif
SettingsSection(
title: L("section_cli_paths"),
caption: L("cli_paths_caption"))
{
self.binaryRow(title: L("codex_binary"), value: self.store.pathDebugInfo.codexBinary)
self.binaryRow(title: L("claude_binary"), value: self.store.pathDebugInfo.claudeBinary)
VStack(alignment: .leading, spacing: 6) {
Text(L("effective_path"))
.font(.callout.weight(.semibold))
ScrollView {
Text(
self.store.pathDebugInfo.effectivePATH.isEmpty
? L("unavailable")
: self.store.pathDebugInfo.effectivePATH)
.font(.system(.footnote, design: .monospaced))
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(6)
}
.frame(minHeight: 60, maxHeight: 110)
.background(Color(NSColor.textBackgroundColor))
.cornerRadius(6)
}
if let loginPATH = self.store.pathDebugInfo.loginShellPATH {
VStack(alignment: .leading, spacing: 6) {
Text(L("login_shell_path"))
.font(.callout.weight(.semibold))
ScrollView {
Text(loginPATH)
.font(.system(.footnote, design: .monospaced))
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(6)
}
.frame(minHeight: 60, maxHeight: 110)
.background(Color(NSColor.textBackgroundColor))
.cornerRadius(6)
}
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 20)
.padding(.vertical, 12)
}
}
private var fileLogPath: String {
CodexBarLog.fileLogURL.path
}
private var animationPatternBinding: Binding<LoadingPattern?> {
Binding(
get: { self.settings.debugLoadingPattern },
set: { self.settings.debugLoadingPattern = $0 })
}
private func replaySelectedAnimation() {
var userInfo: [AnyHashable: Any] = [:]
if let pattern = self.settings.debugLoadingPattern {
userInfo["pattern"] = pattern.rawValue
}
NotificationCenter.default.post(
name: .codexbarDebugReplayAllAnimations,
object: nil,
userInfo: userInfo.isEmpty ? nil : userInfo)
self.store.replayLoadingAnimation(duration: 4)
}
private var displayedLog: String {
if self.logText.isEmpty {
return self.isLoadingLog ? L("loading") : L("no_log_yet_fetch")
}
return self.logText
}
private func loadLog(_ provider: UsageProvider) {
self.isLoadingLog = true
Task {
let text = await ProviderInteractionContext.$current.withValue(.userInitiated) {
await ProviderRefreshContext.$current.withValue(.regular) {
await self.store.debugLog(for: provider)
}
}
await MainActor.run {
self.logText = text
self.isLoadingLog = false
}
}
}
private func saveLog(_ provider: UsageProvider) {
Task {
if self.logText.isEmpty {
self.isLoadingLog = true
let text = await ProviderInteractionContext.$current.withValue(.userInitiated) {
await ProviderRefreshContext.$current.withValue(.regular) {
await self.store.debugLog(for: provider)
}
}
await MainActor.run { self.logText = text }
self.isLoadingLog = false
}
_ = await ProviderInteractionContext.$current.withValue(.userInitiated) {
await ProviderRefreshContext.$current.withValue(.regular) {
await self.store.dumpLog(toFileFor: provider)
}
}
}
}
private func copyToPasteboard(_ text: String) {
let pb = NSPasteboard.general
pb.clearContents()
pb.setString(text, forType: .string)
}
private func binaryRow(title: String, value: String?) -> some View {
VStack(alignment: .leading, spacing: 6) {
Text(title)
.font(.callout.weight(.semibold))
Text(value ?? L("not_found"))
.font(.system(.footnote, design: .monospaced))
.foregroundStyle(value == nil ? .secondary : .primary)
}
}
private func loadClaudeDump() {
self.isLoadingLog = true
Task {
let text = await self.store.debugClaudeDump()
await MainActor.run {
self.logText = text
self.isLoadingLog = false
}
}
}
private func postSessionNotification(_ transition: SessionQuotaTransition, provider: UsageProvider) {
SessionQuotaNotifier().post(transition: transition, provider: provider, badge: 1)
}
private func clearCostCache() async {
guard !self.isClearingCostCache else { return }
self.isClearingCostCache = true
self.costCacheStatus = nil
defer { self.isClearingCostCache = false }
if let error = await self.store.clearCostUsageCache() {
self.costCacheStatus = "Failed: \(error)"
return
}
self.costCacheStatus = L("cleared")
}
private func clearCookieCache() {
let summary = CookieHeaderCache.clearAllDetailed()
if summary.failedCount > 0 {
self.cookieCacheStatus = "Cookie cache cleanup failed for \(summary.failedCount) "
+ "operation\(summary.failedCount == 1 ? "" : "s")."
} else if summary.clearedCount > 0 {
self.cookieCacheStatus = "Cleared \(summary.clearedCount) "
+ "provider\(summary.clearedCount == 1 ? "" : "s")."
} else {
self.cookieCacheStatus = "No cached cookies found."
}
}
private func fetchAttemptsText(for provider: UsageProvider) -> String {
let attempts = self.store.fetchAttempts(for: provider)
guard !attempts.isEmpty else { return L("no_fetch_attempts") }
return attempts.map { attempt in
let kind = Self.fetchKindLabel(attempt.kind)
var line = "\(attempt.strategyID) (\(kind))"
line += attempt.wasAvailable ? " available" : " unavailable"
if let error = attempt.errorDescription, !error.isEmpty {
line += " error=\(error)"
}
return line
}.joined(separator: "\n")
}
private static func fetchKindLabel(_ kind: ProviderFetchKind) -> String {
switch kind {
case .cli: "cli"
case .web: "web"
case .oauth: "oauth"
case .apiToken: "api"
case .localProbe: "local"
case .webDashboard: "web"
}
}
}
@@ -0,0 +1,157 @@
import AppKit
import CodexBarCore
import SwiftUI
enum AppLanguage: String, CaseIterable, Identifiable {
case system = ""
case english = "en"
case chineseSimplified = "zh-Hans"
case chineseTraditional = "zh-Hant"
case japanese = "ja"
case spanish = "es"
case portugueseBrazilian = "pt-BR"
case korean = "ko"
case german = "de"
case french = "fr"
case arabic = "ar"
case italian = "it"
case vietnamese = "vi"
case dutch = "nl"
case turkish = "tr"
case ukrainian = "uk"
case russian = "ru"
case indonesian = "id"
case polish = "pl"
case persian = "fa"
case thai = "th"
case galician = "gl"
case catalan = "ca"
case swedish = "sv"
var id: String {
self.rawValue
}
var label: String {
L(self.labelKey, language: self.labelLanguage)
}
private var labelLanguage: String {
switch self {
case .system, .english:
"en"
default:
self.rawValue
}
}
private var labelKey: String {
switch self {
case .system: "language_system"
case .english: "language_english"
case .chineseSimplified: "language_chinese_simplified"
case .chineseTraditional: "language_chinese_traditional"
case .japanese: "language_japanese"
case .spanish: "language_spanish"
case .portugueseBrazilian: "language_portuguese_brazilian"
case .korean: "language_korean"
case .german: "language_german"
case .french: "language_french"
case .arabic: "language_arabic"
case .italian: "language_italian"
case .vietnamese: "language_vietnamese"
case .dutch: "language_dutch"
case .turkish: "language_turkish"
case .ukrainian: "language_ukrainian"
case .russian: "language_russian"
case .indonesian: "language_indonesian"
case .polish: "language_polish"
case .persian: "language_persian"
case .thai: "language_thai"
case .galician: "language_galician"
case .catalan: "language_catalan"
case .swedish: "language_swedish"
}
}
}
@MainActor
struct GeneralPane: View {
@Bindable var settings: SettingsStore
var body: some View {
Form {
Section {
SettingsMenuPicker(
selection: self.$settings.appLanguage,
options: GeneralSettingsMenuOptions.languages,
label: {
SettingsRowLabel(L("language_title"), subtitle: L("language_subtitle"))
},
optionLabel: { rawValue in
Text(verbatim: AppLanguage(rawValue: rawValue)?.label ?? rawValue)
})
SettingsMenuPicker(
selection: self.$settings.terminalApp,
options: GeneralSettingsMenuOptions.terminalApps(selected: self.settings.terminalApp),
label: {
SettingsRowLabel(L("terminal_app_title"), subtitle: L("terminal_app_subtitle"))
},
optionLabel: { option in
HStack(spacing: 6) {
if let icon = option.pickerIcon {
Image(nsImage: icon)
}
Text(option.label)
}
})
Toggle(L("start_at_login_title"), isOn: self.$settings.launchAtLogin)
} header: {
Text(L("section_system"))
}
Section {
SettingsMenuPicker(
selection: self.$settings.refreshFrequency,
options: GeneralSettingsMenuOptions.refreshFrequencies,
label: { Text(L("refresh_interval_title")) },
optionLabel: { option in Text(option.label) })
Toggle(L("refresh_on_open_title"), isOn: self.$settings.refreshAllProvidersOnMenuOpen)
Toggle(isOn: self.$settings.statusChecksEnabled) {
SettingsRowLabel(
L("check_provider_status_title"),
subtitle: L("check_provider_status_subtitle"))
}
} header: {
Text(L("section_refreshing"))
} footer: {
if self.settings.refreshFrequency == .manual {
SettingsSectionFooter(L("manual_refresh_hint"))
}
}
Section {
LabeledContent(L("open_menu_shortcut_title")) {
OpenMenuShortcutRecorder()
}
} header: {
Text(L("section_keyboard_shortcut"))
}
Section {
HStack {
Spacer()
Button(L("quit_app")) { NSApp.terminate(nil) }
}
}
}
.formStyle(.grouped)
.toggleStyle(.switch)
.scrollContentBackground(.hidden)
.background(FocusResigningBackground())
}
}
@@ -0,0 +1,207 @@
import CodexBarCore
import SwiftUI
@MainActor
struct MenuBarPane: View {
private static let maxOverviewProviders = SettingsStore.mergedOverviewProviderLimit
@State private var isOverviewProviderPopoverPresented = false
@Bindable var settings: SettingsStore
@Bindable var store: UsageStore
static func overviewProviderLimitText(limit: Int = Self.maxOverviewProviders) -> String {
L("overview_choose_providers", String(limit))
}
var body: some View {
Form {
Section {
SettingsMenuPicker(
selection: self.$settings.menuBarIconStyle,
options: MenuBarSettingsMenuOptions.iconStyles,
label: {
SettingsRowLabel(
L("menu_bar_style_title"),
subtitle: L("menu_bar_style_subtitle"))
},
optionLabel: { style in
Text(style.label)
})
SettingsMenuPicker(
selection: self.$settings.menuBarDisplayMode,
options: MenuBarSettingsMenuOptions.displayModes,
label: {
SettingsRowLabel(
L("display_mode_title"),
subtitle: self.settings.menuBarDisplayMode.description)
},
optionLabel: { mode in
Text(mode.label)
})
.disabled(self.settings.menuBarIconStyle != .iconAndPercent)
Toggle(isOn: self.$settings.menuBarShowsResetTimeWhenExhausted) {
SettingsRowLabel(
L("menu_bar_reset_when_exhausted_title"),
subtitle: L("menu_bar_reset_when_exhausted_subtitle"))
}
.disabled(
self.settings.menuBarIconStyle != .iconAndPercent
|| self.settings.menuBarDisplayMode == .resetTime)
} header: {
Text(L("section_icon"))
}
Section {
Toggle(isOn: self.$settings.mergeIcons) {
SettingsRowLabel(L("merge_icons_title"), subtitle: L("merge_icons_subtitle"))
}
SettingsMenuPicker(
selection: self.$settings.switcherRowsOption,
options: MenuBarSettingsMenuOptions.switcherRows,
label: { Text(L("switcher_rows_title")) },
optionLabel: { option in
Text(option.label)
})
.disabled(!self.settings.mergeIcons)
Toggle(isOn: self.$settings.menuBarShowsHighestUsage) {
SettingsRowLabel(
L("show_most_used_provider_title"),
subtitle: L("show_most_used_provider_subtitle"))
}
.disabled(!self.settings.mergeIcons)
self.overviewProviderRow
.disabled(!self.settings.mergeIcons)
} header: {
Text(L("section_combined_icon"))
}
Section {
Toggle(isOn: self.$settings.randomBlinkEnabled) {
SettingsRowLabel(L("surprise_me_title"), subtitle: L("surprise_me_subtitle"))
}
} header: {
Text(L("section_animation"))
}
}
.formStyle(.grouped)
.toggleStyle(.switch)
.scrollContentBackground(.hidden)
.onAppear {
self.reconcileOverviewSelection()
}
.onChange(of: self.settings.mergeIcons) { _, isEnabled in
guard isEnabled else {
self.isOverviewProviderPopoverPresented = false
return
}
self.reconcileOverviewSelection()
}
.onChange(of: self.activeProvidersInOrder) { _, _ in
if self.activeProvidersInOrder.isEmpty {
self.isOverviewProviderPopoverPresented = false
}
self.reconcileOverviewSelection()
}
}
private var overviewProviderRow: some View {
LabeledContent {
if self.showsOverviewConfigureButton {
Button(L("configure")) {
self.isOverviewProviderPopoverPresented = true
}
.popover(isPresented: self.$isOverviewProviderPopoverPresented, arrowEdge: .bottom) {
self.overviewProviderPopover
}
}
} label: {
SettingsRowLabel(L("overview_tab_providers_title"), subtitle: self.overviewProviderSubtitle)
}
}
private var overviewProviderSubtitle: String {
if !self.settings.mergeIcons {
L("overview_enable_merge_icons_hint")
} else if self.activeProvidersInOrder.isEmpty {
L("overview_no_providers_hint")
} else {
self.overviewProviderSelectionSummary
}
}
private var overviewProviderPopover: some View {
VStack(alignment: .leading, spacing: 10) {
Text(Self.overviewProviderLimitText())
.font(.headline)
Text(L("overview_rows_follow_order"))
.font(.footnote)
.foregroundStyle(.tertiary)
ScrollView(.vertical, showsIndicators: true) {
VStack(alignment: .leading, spacing: 6) {
ForEach(self.activeProvidersInOrder, id: \.self) { provider in
Toggle(
isOn: Binding(
get: { self.overviewSelectedProviders.contains(provider) },
set: { shouldSelect in
self.setOverviewProviderSelection(provider: provider, isSelected: shouldSelect)
})) {
Text(self.providerDisplayName(provider))
.font(.body)
}
.toggleStyle(.checkbox)
.disabled(
!self.overviewSelectedProviders.contains(provider) &&
self.overviewSelectedProviders.count >= Self.maxOverviewProviders)
}
}
}
.frame(maxHeight: 220)
}
.padding(12)
.frame(width: 280)
}
private var activeProvidersInOrder: [UsageProvider] {
self.store.enabledProviders()
}
private var overviewSelectedProviders: [UsageProvider] {
self.settings.resolvedMergedOverviewProviders(
activeProviders: self.activeProvidersInOrder,
maxVisibleProviders: Self.maxOverviewProviders)
}
private var showsOverviewConfigureButton: Bool {
self.settings.mergeIcons && !self.activeProvidersInOrder.isEmpty
}
private var overviewProviderSelectionSummary: String {
let selectedNames = self.overviewSelectedProviders.map(self.providerDisplayName)
guard !selectedNames.isEmpty else { return L("overview_no_providers_selected") }
return selectedNames.joined(separator: ", ")
}
private func providerDisplayName(_ provider: UsageProvider) -> String {
ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName
}
private func setOverviewProviderSelection(provider: UsageProvider, isSelected: Bool) {
_ = self.settings.setMergedOverviewProviderSelection(
provider: provider,
isSelected: isSelected,
activeProviders: self.activeProvidersInOrder,
maxVisibleProviders: Self.maxOverviewProviders)
}
private func reconcileOverviewSelection() {
_ = self.settings.reconcileMergedOverviewSelectedProviders(
activeProviders: self.activeProvidersInOrder,
maxVisibleProviders: Self.maxOverviewProviders)
}
}
+203
View File
@@ -0,0 +1,203 @@
import CodexBarCore
import SwiftUI
@MainActor
struct MenuPane: View {
@Bindable var settings: SettingsStore
@Bindable var store: UsageStore
var body: some View {
Form {
Section {
SettingsMenuPicker(
selection: self.$settings.usageBarsFillOption,
options: MenuSettingsMenuOptions.usageBarsFill,
label: { Text(L("usage_bars_fill_title")) },
optionLabel: { option in
Text(option.label)
})
Toggle(isOn: self.$settings.quotaWarningMarkersVisible) {
SettingsRowLabel(
L("show_quota_warning_markers_title"),
subtitle: L("show_quota_warning_markers_subtitle"))
}
SettingsMenuPicker(
selection: self.$settings.weeklyProgressWorkDays,
options: MenuSettingsMenuOptions.weeklyProgressWorkDays,
label: {
Text(L("weekly_progress_work_days_title"))
},
optionLabel: { workDays in
Text(MenuSettingsMenuOptions.weeklyProgressWorkDaysLabel(workDays))
})
SettingsMenuPicker(
selection: self.$settings.resetTimesOption,
options: MenuSettingsMenuOptions.resetTimes,
label: { Text(L("reset_times_title")) },
optionLabel: { option in
Text(option.label)
})
} header: {
Text(L("section_usage"))
}
Section {
Toggle(L("show_provider_changelog_links_title"), isOn: self.$settings.providerChangelogLinksEnabled)
Toggle(isOn: self.$settings.showOptionalCreditsAndExtraUsage) {
SettingsRowLabel(
L("show_credits_extra_usage_title"),
subtitle: L("show_credits_extra_usage_subtitle"))
}
SettingsMenuPicker(
selection: self.$settings.multiAccountMenuLayout,
options: MenuSettingsMenuOptions.multiAccountLayouts,
label: {
Text(L("multi_account_layout_title"))
},
optionLabel: { layout in
Text(layout.label)
})
} header: {
Text(L("section_content"))
}
CostSummarySettingsSection(settings: self.settings, store: self.store)
Section {
Toggle(isOn: self.$settings.agentSessionsEnabled) {
SettingsRowLabel(
L("agent_sessions_title"),
subtitle: L("agent_sessions_subtitle"))
}
TextField(L("agent_sessions_hosts_title"), text: self.$settings.agentSessionsManualHosts)
.disabled(!self.settings.agentSessionsEnabled)
} header: {
Text(L("section_agent_sessions"))
} footer: {
SettingsSectionFooter(L("agent_sessions_footer"))
}
}
.formStyle(.grouped)
.toggleStyle(.switch)
.scrollContentBackground(.hidden)
.background(FocusResigningBackground())
}
}
/// Cost summary settings grouped-form section, including per-provider fetch status in the footer.
@MainActor
struct CostSummarySettingsSection: View {
@Bindable var settings: SettingsStore
@Bindable var store: UsageStore
var body: some View {
Section {
SettingsMenuPicker(
selection: self.$settings.costSummaryOption,
options: MenuSettingsMenuOptions.costSummaries,
label: {
SettingsRowLabel(L("cost_summary_title"), subtitle: L("show_cost_summary_subtitle"))
},
optionLabel: { option in
Text(option.label)
})
if self.settings.costUsageEnabled {
CostHistoryDaysEditor(settings: self.settings)
Toggle(isOn: self.$settings.costComparisonPeriodsEnabled) {
SettingsRowLabel(
L("cost_comparison_periods_title"),
subtitle: L("cost_comparison_periods_subtitle"))
}
}
} header: {
Text(L("section_cost_summary"))
} footer: {
if self.settings.costUsageEnabled {
SettingsSectionFooter {
VStack(alignment: .leading, spacing: 3) {
Text(L("cost_auto_refresh_info"))
self.costStatusLine(provider: .claude)
self.costStatusLine(provider: .codex)
}
}
}
}
}
private func costStatusLine(provider: UsageProvider) -> Text {
let name = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName
guard provider == .claude || provider == .codex else {
return Text(String(format: L("cost_status_unsupported"), name))
}
if self.store.isTokenRefreshInFlight(for: provider) {
let elapsed: String = {
guard let startedAt = self.store.tokenLastAttemptAt(for: provider) else { return "" }
let seconds = max(0, Date().timeIntervalSince(startedAt))
let formatter = DateComponentsFormatter()
formatter.allowedUnits = seconds < 60 ? [.second] : [.minute, .second]
formatter.unitsStyle = .abbreviated
return formatter.string(from: seconds).map { " (\($0))" } ?? ""
}()
return Text(String(format: L("cost_status_fetching"), name, elapsed))
}
if let snapshot = self.store.tokenSnapshot(for: provider) {
let updated = UsageFormatter.updatedString(from: snapshot.updatedAt)
let cost = snapshot.last30DaysCostUSD
.map { UsageFormatter.currencyString($0, currencyCode: snapshot.currencyCode) } ?? ""
let window = snapshot.historyLabel ?? (snapshot.historyDays == 1 ? "today" : "\(snapshot.historyDays)d")
return Text(String(format: L("cost_status_snapshot"), name, updated, window, cost))
}
if let error = self.store.tokenError(for: provider), !error.isEmpty {
let truncated = UsageFormatter.truncatedSingleLine(error, max: 120)
return Text(String(format: L("cost_status_error"), name, truncated))
}
if let lastAttempt = self.store.tokenLastAttemptAt(for: provider) {
let rel = RelativeDateTimeFormatter()
rel.locale = Locale(identifier: "en_US")
rel.unitsStyle = .abbreviated
let when = rel.localizedString(for: lastAttempt, relativeTo: Date())
return Text(String(format: L("cost_status_last_attempt"), name, when))
}
return Text(String(format: L("cost_status_no_data"), name))
}
}
@MainActor
struct CostHistoryDaysEditor: View {
@Bindable var settings: SettingsStore
static func title(days: Int) -> String {
String(format: L("cost_history_days_title"), days)
}
var body: some View {
LabeledContent(Self.title(days: self.settings.costUsageHistoryDays)) {
HStack(spacing: 8) {
TextField(
Self.title(days: self.settings.costUsageHistoryDays),
value: self.$settings.costUsageHistoryDays,
format: .number)
.labelsHidden()
.textFieldStyle(.roundedBorder)
.multilineTextAlignment(.trailing)
.monospacedDigit()
.frame(width: 64)
Stepper(value: self.$settings.costUsageHistoryDays, in: 1...365, step: 1) {
EmptyView()
}
.labelsHidden()
}
}
}
}
@@ -0,0 +1,92 @@
import SwiftUI
/// Menu-backed settings selector that avoids disabled `Picker` items on macOS 27 when built with the macOS 26 SDK.
struct SettingsMenuPicker<Value: Hashable, Label: View, OptionLabel: View>: View {
@Binding private var selection: Value
private let options: [Value]
private let label: () -> Label
private let optionLabel: (Value) -> OptionLabel
init(
selection: Binding<Value>,
options: [Value],
@ViewBuilder label: @escaping () -> Label,
@ViewBuilder optionLabel: @escaping (Value) -> OptionLabel)
{
self._selection = selection
self.options = options
self.label = label
self.optionLabel = optionLabel
}
var body: some View {
LabeledContent {
Menu {
ForEach(self.options, id: \.self) { option in
Button {
self.selection = option
} label: {
HStack {
if self.selection == option {
Image(systemName: "checkmark")
}
self.optionLabel(option)
}
}
}
} label: {
self.optionLabel(self.selection)
.foregroundStyle(.primary)
}
.menuStyle(.button)
.buttonStyle(.borderless)
.fixedSize()
} label: {
self.label()
}
}
}
enum GeneralSettingsMenuOptions {
static let languages = AppLanguage.allCases.map(\.rawValue)
static let refreshFrequencies = RefreshFrequency.allCases
static func terminalApps(selected: TerminalApp) -> [TerminalApp] {
TerminalApp.pickerOptions(selected: selected)
}
static func terminalApps(
selected: TerminalApp,
applicationURL: (String) -> URL?) -> [TerminalApp]
{
TerminalApp.pickerOptions(selected: selected, applicationURL: applicationURL)
}
}
enum MenuBarSettingsMenuOptions {
static let displayModes = MenuBarDisplayMode.allCases
static let iconStyles = MenuBarIconStyle.allCases
static let switcherRows = SwitcherRowsOption.allCases
}
enum MenuSettingsMenuOptions {
static let weeklyProgressWorkDays: [Int?] = [nil, 4, 5, 7]
static let multiAccountLayouts = MultiAccountMenuLayout.allCases
static let usageBarsFill = UsageBarsFillOption.allCases
static let resetTimes = ResetTimesOption.allCases
static let costSummaries = CostSummaryOption.allCases
static func weeklyProgressWorkDaysLabel(_ workDays: Int?) -> String {
switch workDays {
case nil: L("Off")
case 4: L("4 days")
case 5: L("5 days")
case 7: L("7 days")
case let workDays?: L("%d days", workDays)
}
}
}
enum NotificationsSettingsMenuOptions {
static let confettiCelebrations = ConfettiCelebrationOption.allCases
}
@@ -0,0 +1,61 @@
import SwiftUI
@MainActor
struct NotificationsPane: View {
@Bindable var settings: SettingsStore
var body: some View {
Form {
Section {
Toggle(isOn: self.$settings.sessionQuotaNotificationsEnabled) {
SettingsRowLabel(
L("quota_depleted_title"),
subtitle: L("session_quota_notifications_subtitle"))
}
Toggle(isOn: self.$settings.quotaWarningNotificationsEnabled) {
SettingsRowLabel(
L("threshold_warnings_title"),
subtitle: L("quota_warning_notifications_subtitle"))
}
Toggle(isOn: self.$settings.predictivePaceWarningNotificationsEnabled) {
SettingsRowLabel(
L("predictive_pace_warnings_title"),
subtitle: L("predictive_pace_warnings_subtitle"))
}
let warningSettingsVisibility = QuotaWarningSettingsVisibility(
thresholdWarningsEnabled: self.settings.quotaWarningNotificationsEnabled,
predictiveWarningsEnabled: self.settings.predictivePaceWarningNotificationsEnabled)
if warningSettingsVisibility.showsDeliveryControls {
GlobalQuotaWarningSettingsView(
settings: self.settings,
showsThresholdControls: warningSettingsVisibility.showsThresholdControls)
}
} header: {
Text(L("section_alerts"))
}
Section {
SettingsMenuPicker(
selection: self.$settings.confettiCelebrationOption,
options: NotificationsSettingsMenuOptions.confettiCelebrations,
label: {
SettingsRowLabel(
L("confetti_on_reset_title"),
subtitle: L("confetti_on_reset_subtitle"))
},
optionLabel: { option in
Text(option.label)
})
} header: {
Text(L("section_celebrations"))
}
}
.formStyle(.grouped)
.toggleStyle(.switch)
.scrollContentBackground(.hidden)
.background(FocusResigningBackground())
}
}
@@ -0,0 +1,604 @@
import CodexBarCore
import SwiftUI
enum ProviderMetricInlinePresentation: Equatable {
case progress
case status(String)
}
@MainActor
struct ProviderDetailView<SupplementaryContent: View>: View {
let provider: UsageProvider
@Bindable var store: UsageStore
@Binding var isEnabled: Bool
let subtitle: String
let model: UsageMenuCardView.Model
let openAIWebDiagnostic: String?
let settingsPickers: [ProviderSettingsPickerDescriptor]
let settingsToggles: [ProviderSettingsToggleDescriptor]
let settingsFields: [ProviderSettingsFieldDescriptor]
let settingsActions: [ProviderSettingsActionsDescriptor]
let settingsTokenAccounts: ProviderSettingsTokenAccountsDescriptor?
let settingsOrganizations: ProviderSettingsOrganizationsDescriptor?
let errorDisplay: ProviderErrorDisplay?
@Binding var isErrorExpanded: Bool
let onCopyError: (String) -> Void
let onRefresh: () -> Void
let supplementarySettingsContent: SupplementaryContent
let showsSupplementarySettingsContent: Bool
init(
provider: UsageProvider,
store: UsageStore,
isEnabled: Binding<Bool>,
subtitle: String,
model: UsageMenuCardView.Model,
openAIWebDiagnostic: String?,
settingsPickers: [ProviderSettingsPickerDescriptor],
settingsToggles: [ProviderSettingsToggleDescriptor],
settingsFields: [ProviderSettingsFieldDescriptor],
settingsActions: [ProviderSettingsActionsDescriptor] = [],
settingsTokenAccounts: ProviderSettingsTokenAccountsDescriptor?,
settingsOrganizations: ProviderSettingsOrganizationsDescriptor? = nil,
errorDisplay: ProviderErrorDisplay?,
isErrorExpanded: Binding<Bool>,
onCopyError: @escaping (String) -> Void,
onRefresh: @escaping () -> Void,
showsSupplementarySettingsContent: Bool = false,
@ViewBuilder supplementarySettingsContent: () -> SupplementaryContent)
{
self.provider = provider
self.store = store
self._isEnabled = isEnabled
self.subtitle = subtitle
self.model = model
self.openAIWebDiagnostic = openAIWebDiagnostic
self.settingsPickers = settingsPickers
self.settingsToggles = settingsToggles
self.settingsFields = settingsFields
self.settingsActions = settingsActions
self.settingsTokenAccounts = settingsTokenAccounts
self.settingsOrganizations = settingsOrganizations
self.errorDisplay = errorDisplay
self._isErrorExpanded = isErrorExpanded
self.onCopyError = onCopyError
self.onRefresh = onRefresh
self.showsSupplementarySettingsContent = showsSupplementarySettingsContent
self.supplementarySettingsContent = supplementarySettingsContent()
}
static func metricTitle(provider: UsageProvider, metric: UsageMenuCardView.Model.Metric) -> String {
L(UsageMenuCardView.popupMetricTitle(provider: provider, metric: metric))
}
static func metricInlinePresentation(
_ metric: UsageMenuCardView.Model.Metric) -> ProviderMetricInlinePresentation
{
if let statusText = metric.statusText {
return .status(statusText)
}
return .progress
}
static func planRow(provider: UsageProvider, planText: String?) -> (label: String, value: String)? {
guard let rawPlan = planText?.trimmingCharacters(in: .whitespacesAndNewlines),
!rawPlan.isEmpty
else {
return nil
}
guard provider == .openrouter || provider == .mimo || provider == .moonshot || provider == .poe else {
return (label: L("Plan"), value: rawPlan)
}
let prefix = "Balance:"
if rawPlan.hasPrefix(prefix) {
let valueStart = rawPlan.index(rawPlan.startIndex, offsetBy: prefix.count)
let trimmedValue = rawPlan[valueStart...].trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmedValue.isEmpty {
return (label: L("Balance"), value: trimmedValue)
}
}
if provider == .mimo {
return (label: L("Plan"), value: rawPlan)
}
return (label: L("Balance"), value: rawPlan)
}
private var menuBarSettingsPickers: [ProviderSettingsPickerDescriptor] {
self.settingsPickers.filter { $0.placement == .menuBar }
}
private var connectionSettingsPickers: [ProviderSettingsPickerDescriptor] {
self.settingsPickers.filter { $0.placement == .connection }
}
var body: some View {
Form {
Section {
ProviderDetailHeaderRow(
provider: self.provider,
store: self.store,
isEnabled: self.$isEnabled,
subtitle: self.subtitle,
onRefresh: self.onRefresh)
ProviderDetailInfoRows(
provider: self.provider,
store: self.store,
isEnabled: self.isEnabled,
model: self.model)
}
Section {
ProviderMetricsInlineView(
provider: self.provider,
model: self.model,
openAIWebDiagnostic: self.openAIWebDiagnostic,
isEnabled: self.isEnabled)
} header: {
Text(L("Usage"))
}
if let errorDisplay {
Section {
ProviderErrorView(
title: String(
format: L("last_fetch_failed_with_provider"),
self.store.metadata(for: self.provider).displayName),
display: errorDisplay,
isExpanded: self.$isErrorExpanded,
onCopy: { self.onCopyError(errorDisplay.full) })
}
}
if !self.menuBarSettingsPickers.isEmpty {
Section {
ForEach(self.menuBarSettingsPickers) { picker in
ProviderSettingsPickerRowView(picker: picker)
}
} header: {
Text(L("provider_section_menu_bar"))
}
}
if !self.connectionSettingsPickers.isEmpty || !self.settingsActions.isEmpty {
Section {
ForEach(self.connectionSettingsPickers) { picker in
ProviderSettingsPickerRowView(picker: picker)
}
ForEach(self.settingsActions) { descriptor in
ProviderSettingsActionsRowView(descriptor: descriptor)
}
} header: {
Text(L("provider_section_connection"))
}
}
if let tokenAccounts = self.settingsTokenAccounts,
tokenAccounts.isVisible?() ?? true
{
ProviderSettingsTokenAccountsRowView(descriptor: tokenAccounts)
}
ForEach(self.settingsFields) { field in
ProviderSettingsFieldRowView(field: field)
}
if let organizations = self.settingsOrganizations {
ProviderSettingsOrganizationsRowView(descriptor: organizations)
}
if self.showsSupplementarySettingsContent {
self.supplementarySettingsContent
}
ProviderQuotaWarningSettingsView(provider: self.provider, settings: self.store.settings)
if !self.settingsToggles.isEmpty {
Section {
ForEach(self.settingsToggles) { toggle in
ProviderSettingsToggleRowView(toggle: toggle)
}
} header: {
Text(L("Options"))
}
}
}
.formStyle(.grouped)
.scrollContentBackground(.hidden)
}
}
@MainActor
private struct ProviderDetailHeaderRow: View {
let provider: UsageProvider
@Bindable var store: UsageStore
@Binding var isEnabled: Bool
let subtitle: String
let onRefresh: () -> Void
var body: some View {
HStack(alignment: .center, spacing: 12) {
ProviderDetailBrandIcon(provider: self.provider)
VStack(alignment: .leading, spacing: 2) {
Text(self.store.metadata(for: self.provider).displayName)
.font(.title3.weight(.semibold))
Text(self.detailSubtitle)
.font(.footnote)
.foregroundStyle(.secondary)
}
Spacer(minLength: 12)
Button {
self.onRefresh()
} label: {
Image(systemName: "arrow.clockwise")
}
.buttonStyle(.borderless)
.help(L("Refresh"))
Toggle(L("Enabled"), isOn: self.$isEnabled)
.labelsHidden()
.toggleStyle(.switch)
.controlSize(.small)
}
.padding(.vertical, 2)
}
private var detailSubtitle: String {
let lines = self.subtitle.split(separator: "\n", omittingEmptySubsequences: false)
guard lines.count >= 2 else { return self.subtitle }
let first = lines[0]
let rest = lines.dropFirst().joined(separator: "\n")
let tail = rest.trimmingCharacters(in: .whitespacesAndNewlines)
if tail.isEmpty { return String(first) }
return "\(first)\(tail)"
}
}
@MainActor
private struct ProviderDetailBrandIcon: View {
let provider: UsageProvider
var body: some View {
if let brand = ProviderBrandIcon.image(for: self.provider) {
Image(nsImage: brand)
.resizable()
.scaledToFit()
.frame(width: 28, height: 28)
.foregroundStyle(.secondary)
.accessibilityHidden(true)
} else {
Image(systemName: "circle.dotted")
.font(.system(size: 24, weight: .regular))
.foregroundStyle(.secondary)
.accessibilityHidden(true)
}
}
}
@MainActor
private struct ProviderDetailInfoRows: View {
let provider: UsageProvider
@Bindable var store: UsageStore
let isEnabled: Bool
let model: UsageMenuCardView.Model
var body: some View {
ProviderDetailInfoRow(label: L("Source"), value: self.store.sourceLabel(for: self.provider))
ProviderDetailInfoRow(label: L("Version"), value: self.store.version(for: self.provider) ?? L("not detected"))
ProviderDetailInfoRow(label: L("Updated"), value: self.updatedText)
if let status = self.store.status(for: self.provider) {
ProviderDetailInfoRow(label: L("Status"), value: status.description ?? status.indicator.label)
}
if !self.model.email.isEmpty {
ProviderDetailInfoRow(label: L("Account"), value: self.model.email)
}
if self.provider == .kiro,
let authMethod = self.store.snapshot(for: self.provider)?.loginMethod(for: .kiro),
!authMethod.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
ProviderDetailInfoRow(label: L("Auth"), value: authMethod)
}
if let planRow = ProviderDetailView<EmptyView>.planRow(
provider: self.provider,
planText: self.model.planText)
{
ProviderDetailInfoRow(label: planRow.label, value: planRow.value)
}
}
private var updatedText: String {
if let updated = self.store.snapshot(for: self.provider)?.updatedAt {
return UsageFormatter.updatedString(from: updated)
}
if self.store.refreshingProviders.contains(self.provider) {
return L("Refreshing")
}
if self.store.unavailableMessage(for: self.provider) != nil {
return L("Unavailable")
}
return L("Not fetched yet")
}
}
private struct ProviderDetailInfoRow: View {
let label: String
let value: String
var body: some View {
LabeledContent(self.label) {
Text(self.value)
.foregroundStyle(.secondary)
.multilineTextAlignment(.trailing)
.lineLimit(2)
.textSelection(.enabled)
}
}
}
@MainActor
struct ProviderMetricsInlineView: View {
let provider: UsageProvider
let model: UsageMenuCardView.Model
let openAIWebDiagnostic: String?
let isEnabled: Bool
struct InfoRow: Identifiable, Equatable {
enum ID: Hashable {
case credits
case openAIWeb
}
let id: ID
let label: String
let value: String
}
static func infoRows(
for model: UsageMenuCardView.Model,
openAIWebDiagnostic: String?) -> [InfoRow]
{
var rows: [InfoRow] = []
if let credits = model.creditsText {
rows.append(InfoRow(id: .credits, label: L("Credits"), value: credits))
}
if let diagnostic = openAIWebDiagnostic {
rows.append(InfoRow(id: .openAIWeb, label: L("OpenAI web extras"), value: diagnostic))
}
return rows
}
var body: some View {
let hasMetrics = !self.model.metrics.isEmpty
let hasUsageNotes = !self.model.usageNotes.isEmpty
let infoRows = Self.infoRows(for: self.model, openAIWebDiagnostic: self.openAIWebDiagnostic)
let hasProviderCost = self.model.providerCost != nil
let hasTokenUsage = self.model.tokenUsage != nil
let hasResetCredits = self.model.codexResetCredits != nil
if !hasMetrics, !hasUsageNotes, !hasProviderCost, infoRows.isEmpty, !hasTokenUsage, !hasResetCredits {
Text(self.placeholderText)
.font(.footnote)
.foregroundStyle(.secondary)
} else {
ForEach(self.model.metrics, id: \.id) { metric in
ProviderMetricInlineRow(
metric: metric,
title: ProviderDetailView<EmptyView>.metricTitle(provider: self.provider, metric: metric),
progressColor: self.model.progressColor)
}
if hasUsageNotes {
VStack(alignment: .leading, spacing: 4) {
ForEach(Array(self.model.usageNotes.enumerated()), id: \.offset) { _, note in
Text(note)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(2)
.fixedSize(horizontal: false, vertical: true)
}
}
}
ForEach(infoRows) { row in
ProviderDetailInfoRow(label: row.label, value: row.value)
}
if let resetCredits = self.model.codexResetCredits {
ProviderCodexResetCreditsInlineRow(presentation: resetCredits)
}
if let providerCost = self.model.providerCost {
ProviderMetricInlineCostRow(
section: providerCost,
progressColor: self.model.progressColor)
}
if let tokenUsage = self.model.tokenUsage {
ProviderMetricInlineTextRow(title: L("Cost"), value: tokenUsage.sessionLine)
ProviderMetricInlineTextRow(title: "", value: tokenUsage.monthLine)
}
}
}
private var placeholderText: String {
if !self.isEnabled {
return L("Disabled — no recent data")
}
return self.model.placeholder.map(L) ?? L("No usage yet")
}
}
private struct ProviderMetricInlineRow: View {
let metric: UsageMenuCardView.Model.Metric
let title: String
let progressColor: Color
var body: some View {
VStack(alignment: .leading, spacing: 4) {
switch ProviderDetailView<EmptyView>.metricInlinePresentation(self.metric) {
case let .status(statusText):
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(self.title)
.font(.subheadline.weight(.semibold))
.lineLimit(1)
Spacer(minLength: 8)
Text(statusText)
.font(.footnote)
.foregroundStyle(.secondary)
}
case .progress:
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(self.title)
.font(.subheadline.weight(.semibold))
.lineLimit(1)
Spacer(minLength: 8)
Text(self.metric.percentLabel)
.font(.footnote)
.foregroundStyle(.secondary)
.monospacedDigit()
}
UsageProgressBar(
percent: self.metric.percent,
tint: self.progressColor,
accessibilityLabel: self.metric.percentStyle.accessibilityLabel,
pacePercent: self.metric.pacePercent,
paceOnTop: self.metric.paceOnTop,
warningMarkerPercents: self.metric.warningMarkerPercents,
workdayMarkerPercents: self.metric.workdayMarkerPercents)
.frame(maxWidth: .infinity)
let hasLeftDetail = self.metric.detailLeftText?.isEmpty == false
let hasRightDetail = self.metric.detailRightText?.isEmpty == false
let resetText = self.metric.resetText ?? ""
if hasLeftDetail || hasRightDetail || !resetText.isEmpty {
HStack(alignment: .firstTextBaseline, spacing: 8) {
if let leftDetail = self.metric.detailLeftText, !leftDetail.isEmpty {
Text(leftDetail)
.font(.footnote)
.foregroundStyle(.secondary)
}
Spacer(minLength: 8)
if let rightDetail = self.metric.detailRightText, !rightDetail.isEmpty {
Text(rightDetail)
.font(.footnote)
.foregroundStyle(.secondary)
} else if !resetText.isEmpty {
Text(resetText)
.font(.footnote)
.foregroundStyle(.secondary)
}
}
}
if hasRightDetail, !resetText.isEmpty {
Text(resetText)
.font(.footnote)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .trailing)
}
if let detail = self.metric.detailText, !detail.isEmpty {
Text(detail)
.font(.footnote)
.foregroundStyle(.tertiary)
}
}
}
.padding(.vertical, 2)
}
}
private struct ProviderCodexResetCreditsInlineRow: View {
let presentation: CodexResetCreditsPresentation
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(L("Limit Reset Credits"))
.font(.subheadline.weight(.semibold))
Spacer(minLength: 8)
Text(self.presentation.text)
.font(.footnote)
.foregroundStyle(.secondary)
}
HStack(alignment: .firstTextBaseline, spacing: 4) {
Image(systemName: "clock")
.font(.caption2)
Text(self.presentation.expirySummaryText)
.font(.caption)
.foregroundStyle(.tertiary)
.lineLimit(1)
.minimumScaleFactor(0.8)
}
.frame(maxWidth: .infinity, alignment: .trailing)
.accessibilityHidden(true)
}
.padding(.vertical, 2)
.accessibilityElement(children: .combine)
.accessibilityLabel(self.presentation.accessibilityLabel)
}
}
private struct ProviderMetricInlineTextRow: View {
let title: String
let value: String
var body: some View {
HStack(alignment: .firstTextBaseline, spacing: 12) {
if !self.title.isEmpty {
Text(self.title)
.font(.subheadline.weight(.semibold))
}
Spacer(minLength: 8)
Text(self.value)
.font(.footnote)
.foregroundStyle(.secondary)
.multilineTextAlignment(.trailing)
}
}
}
private struct ProviderMetricInlineCostRow: View {
let section: UsageMenuCardView.Model.ProviderCostSection
let progressColor: Color
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(self.section.title)
.font(.subheadline.weight(.semibold))
Spacer(minLength: 8)
if let percentLine = self.section.percentLine {
Text(percentLine)
.font(.footnote)
.foregroundStyle(.secondary)
.monospacedDigit()
}
}
if let percentUsed = self.section.percentUsed {
UsageProgressBar(
percent: percentUsed,
tint: self.progressColor,
accessibilityLabel: L("Usage used"))
.frame(maxWidth: .infinity)
}
Text(self.section.spendLine)
.font(.footnote)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .trailing)
}
.padding(.vertical, 2)
}
}
@@ -0,0 +1,53 @@
import SwiftUI
struct ProviderErrorDisplay {
let preview: String
let full: String
}
@MainActor
struct ProviderErrorView: View {
let title: String
let display: ProviderErrorDisplay
@Binding var isExpanded: Bool
let onCopy: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(self.title)
.font(.footnote.weight(.semibold))
.foregroundStyle(.secondary)
Spacer()
Button {
self.onCopy()
} label: {
Image(systemName: "doc.on.doc")
}
.buttonStyle(.plain)
.foregroundStyle(.secondary)
.help(L("Copy error"))
}
Text(self.display.preview)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(3)
.fixedSize(horizontal: false, vertical: true)
if self.display.preview != self.display.full {
Button(self.isExpanded ? L("Hide details") : L("Show details")) { self.isExpanded.toggle() }
.buttonStyle(.link)
.font(.footnote)
}
if self.isExpanded {
Text(self.display.full)
.font(.footnote)
.textSelection(.enabled)
.fixedSize(horizontal: false, vertical: true)
}
}
.padding(.leading, 2)
}
}
@@ -0,0 +1,8 @@
import AppKit
import SwiftUI
enum ProviderSettingsMetrics {
static let checkboxSize: CGFloat = 18
static let iconSize: CGFloat = 18
static let pickerLabelWidth: CGFloat = 92
}
@@ -0,0 +1,621 @@
import CodexBarCore
import SwiftUI
@MainActor
struct ProviderSettingsToggleRowView: View {
let toggle: ProviderSettingsToggleDescriptor
var body: some View {
let isEnabled = self.toggle.isEnabled?() ?? true
VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .firstTextBaseline, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text(L(self.toggle.title))
.font(.subheadline.weight(.semibold))
.foregroundStyle(isEnabled ? .primary : .tertiary)
Text(L(self.toggle.subtitle))
.font(.footnote)
.foregroundStyle(isEnabled ? .secondary : .tertiary)
.fixedSize(horizontal: false, vertical: true)
}
Spacer(minLength: 8)
Toggle("", isOn: self.toggle.binding)
.labelsHidden()
.toggleStyle(.switch)
}
if self.toggle.binding.wrappedValue {
if let status = self.toggle.statusText?(), !status.isEmpty {
Text(status)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(4)
.fixedSize(horizontal: false, vertical: true)
}
let actions = self.toggle.actions.filter { $0.isVisible?() ?? true }
if !actions.isEmpty {
HStack(spacing: 10) {
ForEach(actions) { action in
Button(L(action.title)) {
Task { @MainActor in
await action.perform()
}
}
.applyProviderSettingsButtonStyle(action.style)
.controlSize(.small)
}
}
}
}
}
.disabled(!isEnabled)
.onChange(of: self.toggle.binding.wrappedValue) { _, enabled in
guard let onChange = self.toggle.onChange else { return }
Task { @MainActor in
await onChange(enabled)
}
}
.task(id: self.toggle.binding.wrappedValue) {
guard self.toggle.binding.wrappedValue else { return }
guard let onAppear = self.toggle.onAppearWhenEnabled else { return }
await onAppear()
}
}
}
@MainActor
struct ProviderSettingsPickerRowView: View {
let picker: ProviderSettingsPickerDescriptor
var body: some View {
let isEnabled = self.picker.isEnabled?() ?? true
let subtitle = self.picker.dynamicSubtitle?() ?? self.picker.subtitle
let trimmedSubtitle = subtitle.trimmingCharacters(in: .whitespacesAndNewlines)
LabeledContent {
HStack(spacing: 8) {
if let trailingText = self.picker.trailingText?(), !trailingText.isEmpty {
Text(trailingText)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.tail)
}
Picker("", selection: self.picker.binding) {
ForEach(self.picker.options) { option in
Text(L(option.title)).tag(option.id)
}
}
.labelsHidden()
.pickerStyle(.menu)
.fixedSize()
}
} label: {
SettingsRowLabel(
L(self.picker.title),
subtitle: trimmedSubtitle.isEmpty ? nil : L(trimmedSubtitle))
}
.disabled(!isEnabled)
.onChange(of: self.picker.binding.wrappedValue) { _, selection in
guard let onChange = self.picker.onChange else { return }
Task { @MainActor in
await onChange(selection)
}
}
}
}
/// Renders a provider settings field descriptor as its own grouped-form section:
/// title becomes the header, subtitle/footer text become the footer, and the
/// placeholder stays inside the field.
@MainActor
struct ProviderSettingsFieldRowView: View {
let field: ProviderSettingsFieldDescriptor
var body: some View {
let trimmedTitle = self.field.title.trimmingCharacters(in: .whitespacesAndNewlines)
Section {
self.fieldView
let actions = self.field.actions.filter { $0.isVisible?() ?? true }
if !actions.isEmpty {
HStack(spacing: 10) {
ForEach(actions) { action in
Button(L(action.title)) {
Task { @MainActor in
await action.perform()
}
}
.applyProviderSettingsButtonStyle(action.style)
.controlSize(.small)
}
}
}
} header: {
if !trimmedTitle.isEmpty {
Text(L(trimmedTitle))
}
} footer: {
self.footerView
}
}
private var fieldView: some View {
let prompt = (self.field.placeholder?.isEmpty == false) ? Text(L(self.field.placeholder ?? "")) : nil
return Group {
switch self.field.kind {
case .plain:
TextField(text: self.field.binding, prompt: prompt) {
EmptyView()
}
case .secure:
SecureField(text: self.field.binding, prompt: prompt) {
EmptyView()
}
}
}
.labelsHidden()
.textFieldStyle(.plain)
.onTapGesture { self.field.onActivate?() }
}
@ViewBuilder
private var footerView: some View {
let trimmedSubtitle = self.field.subtitle.trimmingCharacters(in: .whitespacesAndNewlines)
let footer = (self.field.footerText?.isEmpty == false) ? self.field.footerText : nil
if !trimmedSubtitle.isEmpty || footer != nil {
SettingsSectionFooter {
VStack(alignment: .leading, spacing: 3) {
if !trimmedSubtitle.isEmpty {
Text(L(trimmedSubtitle))
}
if let footer {
Text(L(footer))
}
}
}
}
}
}
@MainActor
struct ProviderSettingsActionsRowView: View {
let descriptor: ProviderSettingsActionsDescriptor
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(L(self.descriptor.title))
.font(.subheadline.weight(.semibold))
if !self.descriptor.subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
Text(L(self.descriptor.subtitle))
.font(.footnote)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
let actions = self.descriptor.actions.filter { $0.isVisible?() ?? true }
if !actions.isEmpty {
HStack(spacing: 10) {
ForEach(actions) { action in
Button(L(action.title)) {
Task { @MainActor in
await action.perform()
}
}
.applyProviderSettingsButtonStyle(action.style)
.controlSize(.small)
}
}
}
}
}
}
@MainActor
struct ProviderSettingsTokenAccountsRowView: View {
struct TeamAccountDraft: Equatable {
var teamMode: Bool
var organizationID: String
var projectID: String
func normalizedForPersistence() -> Self {
guard self.teamMode else {
return Self(teamMode: false, organizationID: "", projectID: "")
}
return Self(
teamMode: true,
organizationID: self.organizationID.trimmingCharacters(in: .whitespacesAndNewlines),
projectID: self.projectID.trimmingCharacters(in: .whitespacesAndNewlines))
}
}
let descriptor: ProviderSettingsTokenAccountsDescriptor
@State private var newLabel: String = ""
@State private var newToken: String = ""
@State private var newOrgID: String = ""
@State private var newProjectID: String = ""
@State private var newTeamMode = false
@State private var teamDrafts: [UUID: TeamAccountDraft] = [:]
var body: some View {
Section {
let accounts = self.descriptor.accounts()
if accounts.isEmpty {
Text(L("No token accounts yet."))
.font(.footnote)
.foregroundStyle(.secondary)
} else {
ForEach(Array(accounts.enumerated()), id: \.element.id) { index, account in
VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .center, spacing: 10) {
Button {
self.descriptor.setActiveIndex(index)
} label: {
HStack(alignment: .center, spacing: 8) {
Image(systemName: self.isActive(index: index, accountCount: accounts.count) ?
"checkmark.circle.fill" : "circle")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(self.isActive(index: index, accountCount: accounts.count) ?
Color.accentColor : Color.secondary)
Text(account.displayName)
.font(
.footnote.weight(
self.isActive(index: index, accountCount: accounts.count) ?
.semibold : .regular))
.foregroundStyle(.primary)
Spacer(minLength: 0)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
Button(L("Remove")) {
self.descriptor.removeAccount(account.id)
}
.buttonStyle(.bordered)
.controlSize(.small)
}
if self.descriptor.showsTeamModeControls {
self.teamModeEditor(account: account)
}
}
}
}
if self.descriptor.primaryAddAction == nil {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 8) {
TextField(text: self.$newLabel, prompt: Text(L("Label"))) {
EmptyView()
}
.labelsHidden()
.textFieldStyle(.roundedBorder)
.font(.footnote)
.frame(maxWidth: 160)
SecureField(text: self.$newToken, prompt: Text(L(self.descriptor.placeholder))) {
EmptyView()
}
.labelsHidden()
.textFieldStyle(.roundedBorder)
.font(.footnote)
Button(L("Add")) {
let label = self.newLabel.trimmingCharacters(in: .whitespacesAndNewlines)
let token = self.newToken.trimmingCharacters(in: .whitespacesAndNewlines)
guard !label.isEmpty, !token.isEmpty else { return }
let orgID = self.descriptor.showsOrganizationField
? self.newOrgID.trimmingCharacters(in: .whitespacesAndNewlines)
: ""
let teamOrgID = self.newOrgID.trimmingCharacters(in: .whitespacesAndNewlines)
let projectID = self.newProjectID.trimmingCharacters(in: .whitespacesAndNewlines)
let usageScope = self.descriptor.showsTeamModeControls
? (self.newTeamMode ? "team" : "personal")
: nil
let accountOrganizationID = if self.newTeamMode {
teamOrgID.isEmpty ? nil : teamOrgID
} else {
orgID.isEmpty ? nil : orgID
}
let accountWorkspaceID = self.newTeamMode && !projectID.isEmpty ? projectID : nil
self.descriptor.addAccount(
label,
token,
usageScope,
accountOrganizationID,
accountWorkspaceID)
self.newLabel = ""
self.newToken = ""
self.newOrgID = ""
self.newProjectID = ""
self.newTeamMode = false
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(Self.isAddDisabled(
label: self.newLabel,
token: self.newToken,
showsTeamModeControls: self.descriptor.showsTeamModeControls,
teamMode: self.newTeamMode,
teamContext: (organizationID: self.newOrgID, projectID: self.newProjectID)))
}
if self.descriptor.showsOrganizationField {
TextField(text: self.$newOrgID, prompt: Text(L("Org ID (optional)"))) {
EmptyView()
}
.labelsHidden()
.textFieldStyle(.roundedBorder)
.font(.footnote)
.help(
L("Optional organization ID for accounts linked to multiple Anthropic organizations."))
}
if self.descriptor.showsTeamModeControls {
Toggle(L("Team mode"), isOn: self.$newTeamMode)
.toggleStyle(.checkbox)
.font(.footnote)
if self.newTeamMode {
HStack(spacing: 8) {
TextField(text: self.$newOrgID, prompt: Text(L("Organization ID"))) {
EmptyView()
}
.labelsHidden()
.textFieldStyle(.roundedBorder)
.font(.footnote)
TextField(text: self.$newProjectID, prompt: Text(L("Project ID"))) {
EmptyView()
}
.labelsHidden()
.textFieldStyle(.roundedBorder)
.font(.footnote)
}
}
}
}
}
HStack(spacing: 10) {
Button(L("Open token file")) {
self.descriptor.openConfigFile()
}
.buttonStyle(.link)
.controlSize(.small)
Button(L("Reload")) {
self.descriptor.reloadFromDisk()
}
.buttonStyle(.link)
.controlSize(.small)
Spacer(minLength: 0)
if let title = self.descriptor.primaryAddActionTitle,
let action = self.descriptor.primaryAddAction
{
Button(L(title)) {
Task { @MainActor in
await action()
}
}
.buttonStyle(.bordered)
.controlSize(.small)
}
}
} header: {
Text(L(self.descriptor.title))
} footer: {
if !self.descriptor.subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
SettingsSectionFooter(L(self.descriptor.subtitle))
}
}
}
private func isActive(index: Int, accountCount: Int) -> Bool {
guard accountCount > 0 else { return false }
let selectedIndex = min(self.descriptor.activeIndex(), max(0, accountCount - 1))
return selectedIndex == index
}
private func teamModeEditor(account: ProviderTokenAccount) -> some View {
let draft = self.teamDraft(for: account)
let original = Self.teamAccountDraft(for: account)
return VStack(alignment: .leading, spacing: 6) {
Toggle(L("Team mode"), isOn: self.teamModeDraftBinding(account: account))
.toggleStyle(.checkbox)
.font(.footnote)
if draft.teamMode {
HStack(spacing: 8) {
TextField(
text: self.organizationIDDraftBinding(account: account),
prompt: Text(L("Organization ID")))
{
EmptyView()
}
.labelsHidden()
.textFieldStyle(.roundedBorder)
.font(.footnote)
TextField(
text: self.projectIDDraftBinding(account: account),
prompt: Text(L("Project ID")))
{
EmptyView()
}
.labelsHidden()
.textFieldStyle(.roundedBorder)
.font(.footnote)
}
}
Button(L("apply")) {
self.applyTeamDraft(account: account)
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(Self.isTeamDraftApplyDisabled(draft: draft, original: original))
}
.padding(.leading, 24)
}
private func teamModeDraftBinding(account: ProviderTokenAccount) -> Binding<Bool> {
Binding(
get: { self.teamDraft(for: account).teamMode },
set: { enabled in
self.updateTeamDraft(account: account) { draft in
draft.teamMode = enabled
}
})
}
private func organizationIDDraftBinding(account: ProviderTokenAccount) -> Binding<String> {
Binding(
get: { self.teamDraft(for: account).organizationID },
set: { value in
self.updateTeamDraft(account: account) { draft in
draft.organizationID = value
}
})
}
private func projectIDDraftBinding(account: ProviderTokenAccount) -> Binding<String> {
Binding(
get: { self.teamDraft(for: account).projectID },
set: { value in
self.updateTeamDraft(account: account) { draft in
draft.projectID = value
}
})
}
private func teamDraft(for account: ProviderTokenAccount) -> TeamAccountDraft {
self.teamDrafts[account.id] ?? Self.teamAccountDraft(for: account)
}
private func updateTeamDraft(
account: ProviderTokenAccount,
mutate: (inout TeamAccountDraft) -> Void)
{
var draft = self.teamDraft(for: account)
mutate(&draft)
self.teamDrafts[account.id] = draft
}
private func applyTeamDraft(account: ProviderTokenAccount) {
let draft = self.teamDraft(for: account)
let original = Self.teamAccountDraft(for: account)
guard !Self.isTeamDraftApplyDisabled(draft: draft, original: original) else { return }
let normalized = draft.normalizedForPersistence()
self.descriptor.updateAccount(
account.id,
normalized.teamMode ? "team" : "personal",
normalized.teamMode ? normalized.organizationID : nil,
normalized.teamMode ? normalized.projectID : nil)
self.teamDrafts[account.id] = nil
}
static func teamAccountDraft(for account: ProviderTokenAccount) -> TeamAccountDraft {
let teamMode = account.sanitizedUsageScope?.lowercased() == "team"
return TeamAccountDraft(
teamMode: teamMode,
organizationID: teamMode ? (account.sanitizedOrganizationID ?? "") : "",
projectID: teamMode ? (account.sanitizedWorkspaceID ?? "") : "")
}
static func isTeamDraftApplyDisabled(draft: TeamAccountDraft, original: TeamAccountDraft) -> Bool {
let draft = draft.normalizedForPersistence()
let original = original.normalizedForPersistence()
guard draft != original else { return true }
guard draft.teamMode else { return false }
return draft.organizationID.isEmpty || draft.projectID.isEmpty
}
static func isAddDisabled(
label: String,
token: String,
showsTeamModeControls: Bool,
teamMode: Bool,
teamContext: (organizationID: String, projectID: String)) -> Bool
{
let label = label.trimmingCharacters(in: .whitespacesAndNewlines)
let token = token.trimmingCharacters(in: .whitespacesAndNewlines)
guard !label.isEmpty, !token.isEmpty else { return true }
guard showsTeamModeControls, teamMode else { return false }
return teamContext.organizationID.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ||
teamContext.projectID.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
}
extension View {
@ViewBuilder
fileprivate func applyProviderSettingsButtonStyle(_ style: ProviderSettingsActionDescriptor.Style) -> some View {
switch style {
case .bordered:
self.buttonStyle(.bordered)
case .link:
self.buttonStyle(.link)
}
}
}
@MainActor
struct ProviderSettingsOrganizationsRowView: View {
let descriptor: ProviderSettingsOrganizationsDescriptor
@State private var errorMessage: String?
@State private var isRefreshing = false
var body: some View {
Section {
let entries = self.descriptor.entries()
if entries.allSatisfy(\.isLocked) {
Text(L("No organizations loaded. Click Refresh after setting your API key."))
.font(.footnote)
.foregroundStyle(.secondary)
} else {
ForEach(entries) { entry in
Toggle(isOn: Binding(
get: { entry.isEnabled },
set: { newValue in
self.descriptor.onToggle(entry.id, newValue)
})) {
VStack(alignment: .leading, spacing: 1) {
Text(entry.localizesTitle ? L(entry.title) : entry.title)
if let subtitle = entry.subtitle,
!subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
Text(entry.localizesSubtitle ? L(subtitle) : subtitle)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
.toggleStyle(.switch)
.controlSize(.small)
.disabled(entry.isLocked)
}
}
HStack(spacing: 10) {
Button(L("Refresh organizations")) {
Task { @MainActor in
self.isRefreshing = true
let result = await self.descriptor.onRefresh()
self.isRefreshing = false
self.errorMessage = result.success ? nil : result.errorMessage
}
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(!self.descriptor.canRefresh() || self.isRefreshing)
if let errorMessage = self.errorMessage, !errorMessage.isEmpty {
Text(errorMessage)
.font(.caption)
.foregroundStyle(.red)
}
}
} header: {
Text(L(self.descriptor.title))
} footer: {
if let subtitle = self.descriptor.subtitle,
!subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
SettingsSectionFooter(L(subtitle))
}
}
}
}
@@ -0,0 +1,289 @@
#if DEBUG
import CodexBarCore
import SwiftUI
extension ProvidersPane {
func _test_binding(for provider: UsageProvider) -> Binding<Bool> {
self.binding(for: provider)
}
func _test_providerSubtitle(_ provider: UsageProvider) -> String {
self.providerSubtitle(provider)
}
func _test_providerSidebarSubtitle(_ provider: UsageProvider) -> String {
self.providerSidebarSubtitle(provider)
}
func _test_moveProviders(fromOffsets: IndexSet, toOffset: Int) {
self.moveProviders(fromOffsets: fromOffsets, toOffset: toOffset)
}
func _test_menuBarMetricPicker(for provider: UsageProvider) -> ProviderSettingsPickerDescriptor? {
self.menuBarMetricPicker(for: provider)
}
func _test_settingsPickers(for provider: UsageProvider) -> [ProviderSettingsPickerDescriptor] {
guard let impl = ProviderCatalog.implementation(for: provider) else { return [] }
var statusTextByID: [String: String] = [:]
var lastAppActiveRunAtByID: [String: Date] = [:]
let context = ProviderSettingsContext(
provider: provider,
settings: self.settings,
store: self.store,
boolBinding: { keyPath in
Binding(
get: { self.settings[keyPath: keyPath] },
set: { self.settings[keyPath: keyPath] = $0 })
},
stringBinding: { keyPath in
Binding(
get: { self.settings[keyPath: keyPath] },
set: { self.settings[keyPath: keyPath] = $0 })
},
statusText: { id in
statusTextByID[id]
},
setStatusText: { id, text in
if let text {
statusTextByID[id] = text
} else {
statusTextByID.removeValue(forKey: id)
}
},
lastAppActiveRunAt: { id in
lastAppActiveRunAtByID[id]
},
setLastAppActiveRunAt: { id, date in
if let date {
lastAppActiveRunAtByID[id] = date
} else {
lastAppActiveRunAtByID.removeValue(forKey: id)
}
},
requestConfirmation: { _ in },
runLoginFlow: {})
return impl.settingsPickers(context: context)
.filter { $0.isVisible?() ?? true }
}
func _test_tokenAccountDescriptor(for provider: UsageProvider) -> ProviderSettingsTokenAccountsDescriptor? {
self.tokenAccountDescriptor(for: provider)
}
func _test_menuCardModel(for provider: UsageProvider) -> UsageMenuCardView.Model {
self.menuCardModel(for: provider)
}
func _test_openAIWebDiagnostic(for provider: UsageProvider) -> String? {
self.openAIWebDiagnostic(for: provider)
}
func _test_providerErrorDisplay(for provider: UsageProvider) -> ProviderErrorDisplay? {
self.providerErrorDisplay(provider)
}
func _test_codexAccountsSectionState() -> CodexAccountsSectionState? {
self.codexAccountsSectionState(for: .codex)
}
func _test_selectCodexVisibleAccount(id: String) async {
await self.selectCodexVisibleAccount(id: id)
}
func _test_addManagedCodexAccount() async {
await self.addManagedCodexAccount()
}
func _test_reauthenticateCodexAccount(_ account: CodexVisibleAccount) async {
await self.reauthenticateCodexAccount(account)
}
func _test_requestCodexSystemVisibleAccount(id: String) async {
await self.requestCodexSystemVisibleAccount(id: id)
}
}
@MainActor
enum ProvidersPaneTestHarness {
static func exercise(settings: SettingsStore, store: UsageStore) {
self.prepareTestState(settings: settings, store: store)
let pane = ProvidersPane(settings: settings, store: store)
self.exercisePaneBasics(pane: pane)
let descriptors = self.makeDescriptors()
self.exerciseDetailViews(store: store, descriptors: descriptors)
}
private static func prepareTestState(settings: SettingsStore, store: UsageStore) {
store.versions[.codex] = "1.0.0"
store.versions[.claude] = "2.0.0 (build 123)"
store.versions[.cursor] = nil
store._setSnapshotForTesting(
UsageSnapshot(primary: nil, secondary: nil, updatedAt: Date()),
provider: .codex)
store._setSnapshotForTesting(
UsageSnapshot(primary: nil, secondary: nil, updatedAt: Date()),
provider: .minimax)
store._setErrorForTesting(String(repeating: "x", count: 200), provider: .cursor)
store.lastSourceLabels[.minimax] = "cookies"
store.refreshingProviders.insert(.codex)
settings.claudeCookieSource = .manual
settings.cursorCookieSource = .manual
settings.opencodeCookieSource = .manual
settings.opencodegoCookieSource = .manual
settings.factoryCookieSource = .manual
settings.minimaxCookieSource = .manual
settings.augmentCookieSource = .manual
}
private static func exercisePaneBasics(pane: ProvidersPane) {
_ = pane._test_binding(for: .codex).wrappedValue
_ = pane._test_providerSubtitle(.codex)
_ = pane._test_providerSubtitle(.claude)
_ = pane._test_providerSubtitle(.cursor)
_ = pane._test_providerSubtitle(.opencode)
_ = pane._test_providerSubtitle(.opencodego)
_ = pane._test_providerSubtitle(.zai)
_ = pane._test_providerSubtitle(.synthetic)
_ = pane._test_providerSubtitle(.minimax)
_ = pane._test_providerSubtitle(.kimi)
_ = pane._test_providerSubtitle(.gemini)
_ = pane._test_menuBarMetricPicker(for: .codex)
_ = pane._test_menuBarMetricPicker(for: .gemini)
_ = pane._test_menuBarMetricPicker(for: .zai)
if let descriptor = pane._test_tokenAccountDescriptor(for: .claude) {
_ = descriptor.isVisible?()
_ = descriptor.accounts()
}
}
private static func exerciseDetailViews(store: UsageStore, descriptors: ProviderListTestDescriptors) {
var isEnabled = true
let enabledBinding = Binding(get: { isEnabled }, set: { isEnabled = $0 })
let pane = ProvidersPane(settings: store.settings, store: store)
let model = pane._test_menuCardModel(for: .codex)
var expanded = false
let expandedBinding = Binding(get: { expanded }, set: { expanded = $0 })
_ = ProviderDetailView(
provider: .codex,
store: store,
isEnabled: enabledBinding,
subtitle: "Subtitle",
model: model,
openAIWebDiagnostic: pane._test_openAIWebDiagnostic(for: .codex),
settingsPickers: [descriptors.picker],
settingsToggles: [descriptors.toggle],
settingsFields: [descriptors.fieldPlain, descriptors.fieldSecure],
settingsTokenAccounts: descriptors.tokenAccountsEmpty,
errorDisplay: ProviderErrorDisplay(preview: "Preview", full: "Full"),
isErrorExpanded: expandedBinding,
onCopyError: { _ in },
onRefresh: {},
showsSupplementarySettingsContent: true,
supplementarySettingsContent: {
Section("Accounts") {
Text("Supplementary")
}
}).body
}
private static func makeDescriptors() -> ProviderListTestDescriptors {
let toggleBinding = Binding(get: { true }, set: { _ in })
let actionBordered = ProviderSettingsActionDescriptor(
id: "action-bordered",
title: "Bordered",
style: .bordered,
isVisible: { true },
perform: { await Task.yield() })
let actionLink = ProviderSettingsActionDescriptor(
id: "action-link",
title: "Link",
style: .link,
isVisible: { true },
perform: { await Task.yield() })
let toggle = ProviderSettingsToggleDescriptor(
id: "toggle",
title: "Toggle",
subtitle: "Toggle subtitle",
binding: toggleBinding,
statusText: { "Status" },
actions: [actionBordered, actionLink],
isVisible: { true },
onChange: nil,
onAppDidBecomeActive: nil,
onAppearWhenEnabled: nil)
let picker = ProviderSettingsPickerDescriptor(
id: "picker",
title: "Picker",
subtitle: "Picker subtitle",
dynamicSubtitle: nil,
binding: Binding(get: { "a" }, set: { _ in }),
options: [
ProviderSettingsPickerOption(id: "a", title: "Option A"),
ProviderSettingsPickerOption(id: "b", title: "Option B"),
],
isVisible: { true },
onChange: nil,
trailingText: { "Trailing" })
let fieldPlain = ProviderSettingsFieldDescriptor(
id: "plain",
title: "Field",
subtitle: "Field subtitle",
kind: .plain,
placeholder: "Placeholder",
binding: Binding(get: { "" }, set: { _ in }),
actions: [actionBordered],
isVisible: { true },
onActivate: nil)
let fieldSecure = ProviderSettingsFieldDescriptor(
id: "secure",
title: "Secure",
subtitle: "Secure subtitle",
kind: .secure,
placeholder: "Secure",
binding: Binding(get: { "" }, set: { _ in }),
actions: [actionLink],
isVisible: { true },
onActivate: nil)
let tokenAccountsEmpty = ProviderSettingsTokenAccountsDescriptor(
id: "accounts-empty",
title: "Accounts",
subtitle: "Accounts subtitle",
placeholder: "Token",
provider: .codex,
isVisible: { true },
accounts: { [] },
activeIndex: { 0 },
setActiveIndex: { _ in },
showsOrganizationField: false,
showsTeamModeControls: false,
addAccount: { _, _, _, _, _ in },
updateAccount: { _, _, _, _ in },
removeAccount: { _ in },
primaryAddActionTitle: nil,
primaryAddAction: nil,
openConfigFile: {},
reloadFromDisk: {})
return ProviderListTestDescriptors(
toggle: toggle,
picker: picker,
fieldPlain: fieldPlain,
fieldSecure: fieldSecure,
tokenAccountsEmpty: tokenAccountsEmpty)
}
}
private struct ProviderListTestDescriptors {
let toggle: ProviderSettingsToggleDescriptor
let picker: ProviderSettingsPickerDescriptor
let fieldPlain: ProviderSettingsFieldDescriptor
let fieldSecure: ProviderSettingsFieldDescriptor
let tokenAccountsEmpty: ProviderSettingsTokenAccountsDescriptor
}
#endif
@@ -0,0 +1,820 @@
import AppKit
import CodexBarCore
import SwiftUI
@MainActor
struct ProvidersPane: View {
let provider: UsageProvider
@Bindable var settings: SettingsStore
@Bindable var store: UsageStore
let managedCodexAccountCoordinator: ManagedCodexAccountCoordinator
let codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator
let codexAmbientLoginRunner: any CodexAmbientLoginRunning
let runProviderLoginFlow: @MainActor (UsageProvider) async -> Void
@State private var expandedErrors: Set<UsageProvider> = []
@State private var settingsStatusTextByID: [String: String] = [:]
@State private var settingsLastAppActiveRunAtByID: [String: Date] = [:]
@State private var activeConfirmation: ProviderSettingsConfirmationState?
@State private var codexAccountsNotice: CodexAccountsSectionNotice?
@State private var isAuthenticatingLiveCodexAccount = false
init(
provider: UsageProvider = .codex,
settings: SettingsStore,
store: UsageStore,
managedCodexAccountCoordinator: ManagedCodexAccountCoordinator = ManagedCodexAccountCoordinator(),
codexAccountPromotionCoordinator: CodexAccountPromotionCoordinator? = nil,
codexAmbientLoginRunner: any CodexAmbientLoginRunning = DefaultCodexAmbientLoginRunner(),
runProviderLoginFlow: @escaping @MainActor (UsageProvider) async -> Void = { _ in })
{
self.provider = provider
self.settings = settings
self.store = store
self.managedCodexAccountCoordinator = managedCodexAccountCoordinator
self.codexAccountPromotionCoordinator = codexAccountPromotionCoordinator
?? CodexAccountPromotionCoordinator(
settingsStore: settings,
usageStore: store,
managedAccountCoordinator: managedCodexAccountCoordinator)
self.codexAmbientLoginRunner = codexAmbientLoginRunner
self.runProviderLoginFlow = runProviderLoginFlow
}
var body: some View {
ProviderDetailView(
provider: self.provider,
store: self.store,
isEnabled: self.binding(for: self.provider),
subtitle: self.providerSubtitle(self.provider),
model: self.menuCardModel(for: self.provider),
openAIWebDiagnostic: self.openAIWebDiagnostic(for: self.provider),
settingsPickers: self.extraSettingsPickers(for: self.provider),
settingsToggles: self.extraSettingsToggles(for: self.provider),
settingsFields: self.extraSettingsFields(for: self.provider),
settingsActions: self.extraSettingsActions(for: self.provider),
settingsTokenAccounts: self.tokenAccountDescriptor(for: self.provider),
settingsOrganizations: self.extraSettingsOrganizations(for: self.provider),
errorDisplay: self.providerErrorDisplay(self.provider),
isErrorExpanded: self.expandedBinding(for: self.provider),
onCopyError: { text in self.copyToPasteboard(text) },
onRefresh: {
self.triggerRefresh(for: self.provider)
},
showsSupplementarySettingsContent: self.codexAccountsSectionState(for: self.provider) != nil,
supplementarySettingsContent: {
if let state = self.codexAccountsSectionState(for: self.provider) {
CodexAccountsSectionView(
state: state,
setActiveVisibleAccount: { visibleAccountID in
Task { @MainActor in
await self.selectCodexVisibleAccount(id: visibleAccountID)
}
},
reauthenticateAccount: { account in
Task { @MainActor in
await self.reauthenticateCodexAccount(account)
}
},
removeAccount: { account in
self.requestManagedCodexAccountRemoval(account)
},
requestSystemVisibleAccount: { visibleAccountID in
Task { @MainActor in
await self.requestCodexSystemVisibleAccount(id: visibleAccountID)
}
},
addAccount: {
Task { @MainActor in
await self.addManagedCodexAccount()
}
})
}
})
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
self.runSettingsDidBecomeActiveHooks()
}
.alert(
self.activeConfirmation?.title ?? "",
isPresented: Binding(
get: { self.activeConfirmation != nil },
set: { isPresented in
if !isPresented { self.activeConfirmation = nil }
}),
actions: {
if let active = self.activeConfirmation {
Button(active.confirmTitle) {
active.onConfirm()
self.activeConfirmation = nil
}
Button(L("cancel"), role: .cancel) { self.activeConfirmation = nil }
}
},
message: {
if let active = self.activeConfirmation {
Text(active.message)
}
})
}
static func filteredProviders(
_ providers: [UsageProvider],
query: String,
displayName: (UsageProvider) -> String) -> [UsageProvider]
{
let trimmedQuery = query.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedQuery.isEmpty else { return providers }
return providers.filter { provider in
displayName(provider).localizedCaseInsensitiveContains(trimmedQuery)
|| provider.rawValue.localizedCaseInsensitiveContains(trimmedQuery)
}
}
func moveProviders(fromOffsets: IndexSet, toOffset: Int) {
guard !self.settings.providersSortedAlphabetically else { return }
self.settings.moveProvider(fromOffsets: fromOffsets, toOffset: toOffset)
}
private func triggerRefresh(for provider: UsageProvider) {
Task { @MainActor in
await ProviderInteractionContext.$current.withValue(.userInitiated) {
if provider == .codex {
await self.store.refreshCodexAccountScopedState(allowDisabled: true)
} else {
await self.store.refreshProvider(provider, allowDisabled: true)
}
}
}
}
func binding(for provider: UsageProvider) -> Binding<Bool> {
let meta = self.store.metadata(for: provider)
return Binding(
get: { self.settings.isProviderEnabled(provider: provider, metadata: meta) },
set: { newValue in
self.settings.setProviderEnabled(provider: provider, metadata: meta, enabled: newValue)
})
}
func providerSubtitle(_ provider: UsageProvider) -> String {
let meta = self.store.metadata(for: provider)
let usageText: String
if self.store.isStale(provider: provider) {
usageText = L("last_fetch_failed")
} else if self.store.knownLimitsAvailability(for: provider)?.isUnavailable == true {
usageText = L("Limits not available")
} else if let snapshot = self.store.snapshot(for: provider) {
let relative = snapshot.updatedAt.relativeDescription()
usageText = relative
} else {
usageText = L("usage_not_fetched_yet")
}
let presentationContext = ProviderPresentationContext(
provider: provider,
settings: self.settings,
store: self.store,
metadata: meta)
let presentation = ProviderCatalog.implementation(for: provider)?
.presentation(context: presentationContext)
?? ProviderPresentation(detailLine: ProviderPresentation.standardDetailLine)
let detailLine = presentation.detailLine(presentationContext)
return "\(detailLine)\n\(usageText)"
}
func providerSidebarSubtitle(_ provider: UsageProvider) -> String {
let meta = self.store.metadata(for: provider)
let usageText: String = if self.store.isStale(provider: provider) {
L("last_fetch_failed")
} else if self.store.knownLimitsAvailability(for: provider)?.isUnavailable == true {
L("Limits not available")
} else if let snapshot = self.store.snapshot(for: provider) {
snapshot.updatedAt.relativeDescription()
} else {
L("usage_not_fetched_yet")
}
let detailLine: String = if let sourceLabel = self.store.lastSourceLabels[provider], !sourceLabel.isEmpty {
sourceLabel
} else if let version = self.store.version(for: provider), !version.isEmpty {
"\(meta.cliName) \(version)"
} else {
meta.cliName
}
return "\(detailLine)\n\(usageText)"
}
func codexAccountsSectionState(for provider: UsageProvider) -> CodexAccountsSectionState? {
guard provider == .codex else { return nil }
let projection = self.settings.codexVisibleAccountProjection
let degradedNotice: CodexAccountsSectionNotice? = if projection.hasUnreadableAddedAccountStore {
CodexAccountsSectionNotice(
text: L("managed_account_storage_unreadable"),
tone: .warning)
} else {
nil
}
return CodexAccountsSectionState(
visibleAccounts: projection.visibleAccounts,
activeVisibleAccountID: projection.activeVisibleAccountID,
liveVisibleAccountID: projection.liveVisibleAccountID,
hasUnreadableManagedAccountStore: projection.hasUnreadableAddedAccountStore,
isAuthenticatingManagedAccount: self.managedCodexAccountCoordinator.isAuthenticatingManagedAccount,
authenticatingManagedAccountID: self.managedCodexAccountCoordinator.authenticatingManagedAccountID,
isRemovingManagedAccount: self.managedCodexAccountCoordinator.isRemovingManagedAccount,
isAuthenticatingLiveAccount: self.isAuthenticatingLiveCodexAccount,
isPromotingSystemAccount: self.codexAccountPromotionCoordinator.isPromotingSystemAccount,
notice: self.codexAccountsNotice ?? degradedNotice)
}
func selectCodexVisibleAccount(id: String) async {
self.codexAccountsNotice = nil
guard self.settings.selectCodexVisibleAccount(id: id) else { return }
await self.refreshCodexProvider()
}
func requestCodexSystemVisibleAccount(id: String) async {
self.codexAccountsNotice = nil
guard let account = self.settings.codexVisibleAccountProjection.visibleAccounts.first(where: { $0.id == id }),
let managedAccountID = account.storedAccountID
else {
return
}
let result = await self.codexAccountPromotionCoordinator.promote(managedAccountID: managedAccountID)
if case let .failure(error) = result {
self.codexAccountsNotice = CodexAccountsSectionNotice(text: error.message, tone: .warning)
}
}
func addManagedCodexAccount() async {
self.codexAccountsNotice = nil
guard let state = self.codexAccountsSectionState(for: .codex), state.canAddAccount else {
return
}
do {
let account = try await self.managedCodexAccountCoordinator.authenticateManagedAccount()
self.selectCodexVisibleAccountForAuthenticatedManagedAccount(account)
await self.refreshCodexProvider()
} catch {
self.codexAccountsNotice = self.codexAccountsNotice(for: error)
}
}
func reauthenticateCodexAccount(_ account: CodexVisibleAccount) async {
self.codexAccountsNotice = nil
if let accountID = account.storedAccountID {
guard let state = self.codexAccountsSectionState(for: .codex), state.canReauthenticate(account) else {
return
}
do {
_ = try await self.managedCodexAccountCoordinator
.authenticateManagedAccount(existingAccountID: accountID)
await self.refreshCodexProvider()
} catch {
self.codexAccountsNotice = self.codexAccountsNotice(for: error)
}
return
}
guard let state = self.codexAccountsSectionState(for: .codex), state.canReauthenticate(account) else {
return
}
self.isAuthenticatingLiveCodexAccount = true
self.codexAccountPromotionCoordinator.setLiveReauthenticationInProgress(true)
defer {
self.isAuthenticatingLiveCodexAccount = false
self.codexAccountPromotionCoordinator.setLiveReauthenticationInProgress(false)
}
let result = await self.codexAmbientLoginRunner.run(timeout: 120)
if let info = CodexLoginAlertPresentation.alertInfo(for: result) {
self.presentLoginAlert(title: info.title, message: info.message)
return
}
await self.refreshCodexProvider()
}
func removeManagedCodexAccount(id: UUID) async {
self.codexAccountsNotice = nil
do {
try await self.managedCodexAccountCoordinator.removeManagedAccount(id: id)
await self.refreshCodexProvider()
} catch {
self.codexAccountsNotice = self.codexAccountsNotice(for: error)
}
}
func requestManagedCodexAccountRemoval(_ account: CodexVisibleAccount) {
guard let accountID = account.storedAccountID else { return }
self.activeConfirmation = ProviderSettingsConfirmationState(
title: L("remove_codex_account_title"),
message: String(format: L("remove_account_message"), account.email),
confirmTitle: L("remove"),
onConfirm: {
Task { @MainActor in
await self.removeManagedCodexAccount(id: accountID)
}
})
}
func providerErrorDisplay(_ provider: UsageProvider) -> ProviderErrorDisplay? {
guard let full = self.store.error(for: provider), !full.isEmpty else { return nil }
let preview = self.store.userFacingError(for: provider) ?? full
return ProviderErrorDisplay(
preview: self.truncated(preview, prefix: ""),
full: full)
}
private func extraSettingsToggles(for provider: UsageProvider) -> [ProviderSettingsToggleDescriptor] {
guard let impl = ProviderCatalog.implementation(for: provider) else { return [] }
let context = self.makeSettingsContext(provider: provider)
return impl.settingsToggles(context: context)
.filter { $0.isVisible?() ?? true }
}
private func extraSettingsPickers(for provider: UsageProvider) -> [ProviderSettingsPickerDescriptor] {
guard let impl = ProviderCatalog.implementation(for: provider) else { return [] }
let context = self.makeSettingsContext(provider: provider)
let providerPickers = impl.settingsPickers(context: context)
.filter { $0.isVisible?() ?? true }
if let menuBarPicker = self.menuBarMetricPicker(for: provider) {
return [menuBarPicker] + providerPickers
}
return providerPickers
}
private func extraSettingsFields(for provider: UsageProvider) -> [ProviderSettingsFieldDescriptor] {
guard let impl = ProviderCatalog.implementation(for: provider) else { return [] }
let context = self.makeSettingsContext(provider: provider)
return impl.settingsFields(context: context)
.filter { $0.isVisible?() ?? true }
}
private func extraSettingsActions(for provider: UsageProvider) -> [ProviderSettingsActionsDescriptor] {
guard let impl = ProviderCatalog.implementation(for: provider) else { return [] }
let context = self.makeSettingsContext(provider: provider)
return impl.settingsActions(context: context)
.filter { $0.isVisible?() ?? true }
}
private func extraSettingsOrganizations(
for provider: UsageProvider) -> ProviderSettingsOrganizationsDescriptor?
{
guard let impl = ProviderCatalog.implementation(for: provider) else { return nil }
let context = self.makeSettingsContext(provider: provider)
return impl.settingsOrganizations(context: context)
}
func tokenAccountDescriptor(for provider: UsageProvider) -> ProviderSettingsTokenAccountsDescriptor? {
guard let support = TokenAccountSupportCatalog.support(for: provider) else { return nil }
let context = self.makeSettingsContext(provider: provider)
return ProviderSettingsTokenAccountsDescriptor(
id: "token-accounts-\(provider.rawValue)",
title: support.title,
subtitle: support.subtitle,
placeholder: support.placeholder,
provider: provider,
isVisible: {
ProviderCatalog.implementation(for: provider)?
.tokenAccountsVisibility(context: context, support: support)
?? (!support.requiresManualCookieSource ||
!context.settings.tokenAccounts(for: provider).isEmpty)
},
accounts: { self.settings.tokenAccounts(for: provider) },
activeIndex: {
let data = self.settings.tokenAccountsData(for: provider)
return data?.clampedActiveIndex() ?? 0
},
setActiveIndex: { index in
self.settings.setActiveTokenAccountIndex(index, for: provider)
Task { @MainActor in
await ProviderInteractionContext.$current.withValue(.userInitiated) {
await self.store.refreshProvider(provider, allowDisabled: true)
}
}
},
showsOrganizationField: provider == .claude,
showsTeamModeControls: provider == .zai,
addAccount: { label, token, usageScope, organizationID, workspaceID in
self.settings.addTokenAccount(
provider: provider,
label: label,
token: token,
usageScope: usageScope,
organizationID: organizationID,
workspaceID: workspaceID)
Task { @MainActor in
await ProviderInteractionContext.$current.withValue(.userInitiated) {
await self.store.refreshProvider(provider, allowDisabled: true)
}
}
},
updateAccount: { accountID, usageScope, organizationID, workspaceID in
self.settings.updateTokenAccount(
provider: provider,
accountID: accountID,
usageScope: usageScope,
organizationID: organizationID,
workspaceID: workspaceID)
Task { @MainActor in
await ProviderInteractionContext.$current.withValue(.userInitiated) {
await self.store.refreshProvider(provider, allowDisabled: true)
}
}
},
removeAccount: { accountID in
self.settings.removeTokenAccount(provider: provider, accountID: accountID)
Task { @MainActor in
await ProviderInteractionContext.$current.withValue(.userInitiated) {
await self.store.refreshProvider(provider, allowDisabled: true)
}
}
},
primaryAddActionTitle: provider == .copilot ? "Add Account" : nil,
primaryAddAction: provider == .copilot ? {
await CopilotLoginFlow.run(settings: self.settings)
await ProviderInteractionContext.$current.withValue(.userInitiated) {
await self.store.refreshProvider(provider, allowDisabled: true)
}
} : nil,
openConfigFile: {
self.settings.openTokenAccountsFile()
},
reloadFromDisk: {
self.settings.reloadTokenAccounts()
Task { @MainActor in
await ProviderInteractionContext.$current.withValue(.userInitiated) {
await self.store.refreshProvider(provider, allowDisabled: true)
}
}
})
}
private func makeSettingsContext(provider: UsageProvider) -> ProviderSettingsContext {
ProviderSettingsContext(
provider: provider,
settings: self.settings,
store: self.store,
boolBinding: { keyPath in
Binding(
get: { self.settings[keyPath: keyPath] },
set: { self.settings[keyPath: keyPath] = $0 })
},
stringBinding: { keyPath in
Binding(
get: { self.settings[keyPath: keyPath] },
set: { self.settings[keyPath: keyPath] = $0 })
},
statusText: { id in
self.settingsStatusTextByID[id]
},
setStatusText: { id, text in
if let text {
self.settingsStatusTextByID[id] = text
} else {
self.settingsStatusTextByID.removeValue(forKey: id)
}
},
lastAppActiveRunAt: { id in
self.settingsLastAppActiveRunAtByID[id]
},
setLastAppActiveRunAt: { id, date in
if let date {
self.settingsLastAppActiveRunAtByID[id] = date
} else {
self.settingsLastAppActiveRunAtByID.removeValue(forKey: id)
}
},
requestConfirmation: { confirmation in
self.activeConfirmation = ProviderSettingsConfirmationState(confirmation: confirmation)
},
runLoginFlow: {
await self.runProviderLoginFlow(provider)
})
}
func menuBarMetricPicker(for provider: UsageProvider) -> ProviderSettingsPickerDescriptor? {
let options: [ProviderSettingsPickerOption]
if provider == .openrouter {
options = [
ProviderSettingsPickerOption(id: MenuBarMetricPreference.automatic.rawValue, title: L("automatic")),
ProviderSettingsPickerOption(
id: MenuBarMetricPreference.primary.rawValue,
title: L("primary_api_key_limit")),
]
} else if provider == .mistral {
options = [
ProviderSettingsPickerOption(
id: MenuBarMetricPreference.automatic.rawValue,
title: L("metric_mistral_payg")),
ProviderSettingsPickerOption(
id: MenuBarMetricPreference.monthlyPlan.rawValue,
title: L("metric_mistral_monthly_plan")),
]
} else if SettingsStore.isBalanceOnlyProvider(provider) {
options = [
ProviderSettingsPickerOption(id: MenuBarMetricPreference.automatic.rawValue, title: L("Automatic")),
]
} else if provider == .mimo {
let snapshot = self.store.snapshot(for: provider)
var metricOptions = [
ProviderSettingsPickerOption(id: MenuBarMetricPreference.automatic.rawValue, title: L("automatic")),
]
if snapshot?.primary != nil, snapshot?.mimoUsage != nil {
metricOptions.append(ProviderSettingsPickerOption(
id: MenuBarMetricPreference.primary.rawValue,
title: String(format: L("metric_primary"), L("Credits"))))
metricOptions.append(ProviderSettingsPickerOption(
id: MenuBarMetricPreference.secondary.rawValue,
title: String(format: L("metric_secondary"), L("Balance"))))
}
options = metricOptions
} else if provider == .abacus {
let metadata = self.store.metadata(for: provider)
options = [
ProviderSettingsPickerOption(id: MenuBarMetricPreference.automatic.rawValue, title: L("automatic")),
ProviderSettingsPickerOption(
id: MenuBarMetricPreference.primary.rawValue,
title: String(format: L("metric_primary"), metadata.sessionLabel)),
]
} else {
let metadata = self.store.metadata(for: provider)
let snapshot = self.store.snapshot(for: provider)
let supportsAverage = self.settings.menuBarMetricSupportsAverage(for: provider)
let supportsPrimaryAndSecondary = self.settings.menuBarMetricSupportsPrimaryAndSecondary(for: provider)
let supportsTertiary = self.settings.menuBarMetricSupportsTertiary(for: provider, snapshot: snapshot)
let supportsExtraUsage = self.settings.menuBarMetricSupportsExtraUsage(for: provider, snapshot: snapshot)
var metricOptions: [ProviderSettingsPickerOption] = [
ProviderSettingsPickerOption(id: MenuBarMetricPreference.automatic.rawValue, title: L("automatic")),
ProviderSettingsPickerOption(
id: MenuBarMetricPreference.primary.rawValue,
title: String(format: L("metric_primary"), metadata.sessionLabel)),
ProviderSettingsPickerOption(
id: MenuBarMetricPreference.secondary.rawValue,
title: String(format: L("metric_secondary"), metadata.weeklyLabel)),
]
if supportsPrimaryAndSecondary {
metricOptions.append(ProviderSettingsPickerOption(
id: MenuBarMetricPreference.primaryAndSecondary.rawValue,
title: "\(L(metadata.sessionLabel)) + \(L(metadata.weeklyLabel))"))
}
if supportsTertiary {
let tertiaryTitle = metadata.opusLabel ?? MenuBarMetricPreference.tertiary.label
metricOptions.append(ProviderSettingsPickerOption(
id: MenuBarMetricPreference.tertiary.rawValue,
title: String(format: L("metric_tertiary"), tertiaryTitle)))
}
if supportsExtraUsage {
metricOptions.append(ProviderSettingsPickerOption(
id: MenuBarMetricPreference.extraUsage.rawValue,
title: MenuBarMetricPreference.extraUsage.label))
}
if supportsAverage {
metricOptions.append(ProviderSettingsPickerOption(
id: MenuBarMetricPreference.average.rawValue,
title: String(format: L("metric_average"), metadata.sessionLabel, metadata.weeklyLabel)))
}
options = metricOptions
}
return ProviderSettingsPickerDescriptor(
id: "menuBarMetric",
title: L("menu_bar_metric_title"),
subtitle: Self.menuBarMetricPickerSubtitle(for: provider),
placement: .menuBar,
binding: Binding(
get: {
self.settings
.menuBarMetricPreference(for: provider, snapshot: self.store.snapshot(for: provider))
.rawValue
},
set: { rawValue in
guard let preference = MenuBarMetricPreference(rawValue: rawValue) else { return }
self.settings.setMenuBarMetricPreference(preference, for: provider)
}),
options: options,
isVisible: { true },
onChange: nil)
}
private static func menuBarMetricPickerSubtitle(for provider: UsageProvider) -> String {
switch provider {
case .deepseek:
L("menu_bar_metric_subtitle_deepseek")
case .moonshot:
L("menu_bar_metric_subtitle_moonshot")
case .mistral:
L("menu_bar_metric_subtitle_mistral")
case .kimik2:
L("menu_bar_metric_subtitle_kimik2")
default:
L("menu_bar_metric_subtitle")
}
}
func menuCardModel(for provider: UsageProvider) -> UsageMenuCardView.Model {
let metadata = self.store.metadata(for: provider)
let snapshot = self.store.snapshot(for: provider)
let now = Date()
let codexProjection = self.store.codexConsumerProjectionIfNeeded(
for: provider,
surface: .liveCard,
now: now)
let credits: CreditsSnapshot?
let creditsError: String?
let dashboard: OpenAIDashboardSnapshot?
let dashboardError: String?
let tokenSnapshot: CostUsageTokenSnapshot?
let tokenError: String?
if let codexProjection {
credits = codexProjection.credits?.snapshot
creditsError = codexProjection.credits?.userFacingError
dashboard = nil
dashboardError = codexProjection.userFacingErrors.dashboard
tokenSnapshot = self.store.tokenSnapshot(for: provider)
tokenError = self.store.tokenError(for: provider)
} else if ProviderDescriptorRegistry.descriptor(for: provider).tokenCost.supportsTokenCost {
credits = nil
creditsError = nil
dashboard = nil
dashboardError = nil
tokenSnapshot = self.store.tokenSnapshot(for: provider)
tokenError = self.store.tokenError(for: provider)
} else {
credits = nil
creditsError = nil
dashboard = nil
dashboardError = nil
tokenSnapshot = nil
tokenError = nil
}
// Abacus uses primary for monthly credits (no secondary window)
let paceWindow = provider == .abacus ? snapshot?.primary : snapshot?.secondary
let weeklyPace = if let codexProjection,
let weekly = codexProjection.rateWindow(for: .weekly)
{
self.store.weeklyPace(provider: provider, window: weekly, now: now)
} else {
paceWindow.flatMap { window in
self.store.weeklyPace(provider: provider, window: window, now: now)
}
}
let input = UsageMenuCardView.Model.Input(
provider: provider,
metadata: metadata,
snapshot: snapshot,
codexProjection: codexProjection,
credits: credits,
creditsError: creditsError,
dashboard: dashboard,
dashboardError: dashboardError,
tokenSnapshot: tokenSnapshot,
tokenError: tokenError,
account: self.store.accountInfo(for: provider),
isRefreshing: self.store.refreshingProviders.contains(provider),
lastError: codexProjection?.userFacingErrors.usage ?? self.store.userFacingError(for: provider),
limitsAvailability: self.store.knownLimitsAvailability(for: provider),
usageBarsShowUsed: self.settings.usageBarsShowUsed,
resetTimeDisplayStyle: self.settings.resetTimeDisplayStyle,
tokenCostUsageEnabled: self.settings.isCostUsageEffectivelyEnabled(for: provider),
tokenCostInlineDashboardEnabled: self.settings.costSummaryShowsInlineDashboard(for: provider),
// Display style only controls the main menu. Provider details always expose
// available cost data in their Usage section.
tokenCostMenuSectionEnabled: self.settings.isCostUsageEffectivelyEnabled(for: provider),
showOptionalCreditsAndExtraUsage: self.settings.showOptionalCreditsAndExtraUsage,
codexSparkUsageVisible: self.settings.codexSparkUsageVisible,
copilotBudgetExtrasEnabled: self.settings.copilotBudgetExtrasEnabled,
hidePersonalInfo: self.settings.hidePersonalInfo,
weeklyPace: weeklyPace,
quotaWarningThresholds: [
.session: self.quotaWarningMarkerThresholds(provider: provider, window: .session),
.weekly: self.quotaWarningMarkerThresholds(provider: provider, window: .weekly),
],
workDaysPerWeek: self.settings.weeklyProgressWorkDays,
now: now)
return UsageMenuCardView.Model.make(input)
}
func openAIWebDiagnostic(for provider: UsageProvider) -> String? {
guard provider == .codex else { return nil }
let diagnostic = self.store.codexConsumerProjectionIfNeeded(
for: provider,
surface: .liveCard)?.userFacingErrors.dashboard
return PersonalInfoRedactor.redactEmails(in: diagnostic, isEnabled: self.settings.hidePersonalInfo)
}
private func quotaWarningMarkerThresholds(provider: UsageProvider, window: QuotaWarningWindow) -> [Int] {
guard self.settings.quotaWarningMarkersVisible else { return [] }
guard self.settings.quotaWarningEnabled(provider: provider, window: window) else { return [] }
return self.settings.resolvedQuotaWarningThresholds(provider: provider, window: window)
}
private func refreshCodexProvider() async {
await ProviderInteractionContext.$current.withValue(.userInitiated) {
await self.store.refreshCodexAccountScopedState(allowDisabled: true)
}
}
private func selectCodexVisibleAccountForAuthenticatedManagedAccount(_ account: ManagedCodexAccount) {
self.settings.selectAuthenticatedManagedCodexAccount(account)
}
private func codexAccountsNotice(for error: Error) -> CodexAccountsSectionNotice {
if let error = error as? ManagedCodexAccountCoordinatorError,
error == .authenticationInProgress
{
return CodexAccountsSectionNotice(
text: L("managed_login_already_running"),
tone: .warning)
}
if let error = error as? ManagedCodexAccountServiceError {
return CodexAccountsSectionNotice(text: error.userFacingMessage, tone: .warning)
}
return CodexAccountsSectionNotice(
text: error.localizedDescription,
tone: .warning)
}
private func presentLoginAlert(title: String, message: String) {
let alert = NSAlert()
alert.messageText = L(title)
alert.informativeText = L(message)
alert.alertStyle = .warning
alert.runModal()
}
private func runSettingsDidBecomeActiveHooks() {
for provider in UsageProvider.allCases {
for toggle in self.extraSettingsToggles(for: provider) {
guard let hook = toggle.onAppDidBecomeActive else { continue }
Task { @MainActor in
await hook()
}
}
}
}
private func truncated(_ text: String, prefix: String, maxLength: Int = 160) -> String {
var message = text.trimmingCharacters(in: .whitespacesAndNewlines)
if message.count > maxLength {
let idx = message.index(message.startIndex, offsetBy: maxLength)
message = "\(message[..<idx])"
}
return prefix + message
}
private func expandedBinding(for provider: UsageProvider) -> Binding<Bool> {
Binding(
get: { self.expandedErrors.contains(provider) },
set: { expanded in
if expanded {
self.expandedErrors.insert(provider)
} else {
self.expandedErrors.remove(provider)
}
})
}
private func copyToPasteboard(_ text: String) {
let pb = NSPasteboard.general
pb.clearContents()
pb.setString(text, forType: .string)
}
}
@MainActor
struct ProviderSettingsConfirmationState: Identifiable {
let id = UUID()
let title: String
let message: String
let confirmTitle: String
let onConfirm: () -> Void
init(
title: String,
message: String,
confirmTitle: String,
onConfirm: @escaping () -> Void)
{
self.title = title
self.message = message
self.confirmTitle = confirmTitle
self.onConfirm = onConfirm
}
init(confirmation: ProviderSettingsConfirmation) {
self.title = L(confirmation.title)
self.message = L(confirmation.message)
self.confirmTitle = L(confirmation.confirmTitle)
self.onConfirm = confirmation.onConfirm
}
}
@@ -0,0 +1,61 @@
import CodexBarCore
import Foundation
import Observation
extension SettingsPane {
/// Stable token used to remember the selected pane across launches.
var persistenceToken: String {
switch self {
case .general: "general"
case .notifications: "notifications"
case .menuBar: "menuBar"
case .menu: "menu"
case .advanced: "advanced"
case .about: "about"
case .debug: "debug"
case let .provider(provider): "provider:\(provider.rawValue)"
}
}
init?(persistenceToken: String) {
switch persistenceToken {
case "general": self = .general
case "notifications": self = .notifications
case "menuBar": self = .menuBar
// Pre-0.41.1 releases persisted the retired Display pane; its contents moved to Menu Bar.
case "display": self = .menuBar
case "menu": self = .menu
case "advanced": self = .advanced
case "about": self = .about
case "debug": self = .debug
default:
let providerPrefix = "provider:"
guard persistenceToken.hasPrefix(providerPrefix),
let provider = UsageProvider(rawValue: String(persistenceToken.dropFirst(providerPrefix.count)))
else {
return nil
}
self = .provider(provider)
}
}
}
@MainActor
@Observable
final class PreferencesSelection {
static let paneDefaultsKey = "settingsSelectedPane"
private let userDefaults: UserDefaults
var pane: SettingsPane {
didSet {
self.userDefaults.set(self.pane.persistenceToken, forKey: Self.paneDefaultsKey)
}
}
init(userDefaults: UserDefaults = .standard) {
self.userDefaults = userDefaults
let token = userDefaults.string(forKey: Self.paneDefaultsKey) ?? ""
self.pane = SettingsPane(persistenceToken: token) ?? .general
}
}

Some files were not shown because too many files have changed in this diff Show More