import CodexBarCore import Foundation struct CodexUIErrorMapper { private static var codexCLINotSignedInMessage: String { L("Codex CLI is not signed in. Run `codex login --device-auth`, then refresh.") } static func userFacingMessage(_ raw: String?) -> String? { guard let raw, !raw.isEmpty else { return nil } let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return nil } let lower = trimmed.lowercased() if self.isAlreadyUserFacing(lower: lower) { return trimmed } if let cachedMessage = self.cachedMessage(raw: trimmed, lower: lower) { return cachedMessage } if self.looksCodexCLIMissing(lower: lower) { return L("Codex CLI missing. Install via `npm i -g @openai/codex` (or bun install) and restart.") } if self.looksCodexCLILoginRequired(lower: lower) { return self.codexCLINotSignedInMessage } if self.looksExpired(lower: lower) { return L("Codex session expired. Sign in again.") } if lower.contains("frame load interrupted") { return L("OpenAI web refresh was interrupted. Refresh OpenAI cookies and try again.") } if self.looksOpenAIWebTimeout(lower: lower) { return L("OpenAI web refresh timed out. Refresh OpenAI cookies and try again.") } if self.looksOpenAIWebNetworkError(lower: lower) { return L( "OpenAI web refresh hit a network error. " + "Check your connection, then refresh OpenAI cookies and try again.") } if self.looksInternalTransport(lower: lower) { return L("Codex usage is temporarily unavailable. Try refreshing.") } return trimmed } private static func cachedMessage(raw: String, lower: String) -> String? { let cachedMarker = " Cached values from " guard let suffixRange = raw.range(of: cachedMarker) else { return nil } let rawPrefix = String(raw[.. String { let droppedPrefix = if rawPrefix.lowercased().hasPrefix(prefix.lowercased()) { String(rawPrefix.dropFirst(prefix.count)) } else { rawPrefix } var message = droppedPrefix.trimmingCharacters(in: .whitespacesAndNewlines) if message.hasSuffix(".") { message.removeLast() } return message } private static func cachedStamp(raw: String, suffixRange: Range, marker: String) -> String { let start = raw.index(suffixRange.lowerBound, offsetBy: marker.count) var stamp = String(raw[start...]).trimmingCharacters(in: .whitespacesAndNewlines) if stamp.hasSuffix(".") { stamp.removeLast() } return stamp } private static func isAlreadyUserFacing(lower: String) -> Bool { lower.contains("openai cookies are for") || lower.contains("sign in to chatgpt.com") || lower.contains("requires a signed-in chatgpt.com session") || lower.contains("managed codex account data is unavailable") || lower.contains("selected managed codex account is unavailable") || lower.contains("codex credits are still loading") || lower.contains("codex account changed; importing browser cookies") || lower.contains("codex cli is not signed in.") || lower.contains("codex session expired. sign in again.") || lower.contains("openai web refresh timed out. refresh openai cookies and try again.") || lower.contains( "openai web refresh hit a network error. " + "check your connection, then refresh openai cookies and try again.") || lower.contains("codex usage is temporarily unavailable. try refreshing.") } private static func looksCodexCLIMissing(lower: String) -> Bool { lower.contains("codex cli missing") || lower.contains("codex cli not found") || lower.contains("missing cli codex") || lower.contains("missing cli 'codex'") || lower.contains("missing cli \"codex\"") || (lower.contains("binary not found") && lower.contains("codex")) } private static func looksCodexCLILoginRequired(lower: String) -> Bool { lower.contains("codex account authentication required") || lower.contains("account authentication required to read rate limits") || lower.contains("requiresopenaiauth") } private static func looksExpired(lower: String) -> Bool { lower.contains("token_expired") || lower.contains("authentication token is expired") || lower.contains("oauth token has expired") || lower.contains("provided authentication token is expired") || lower.contains("please try signing in again") || lower.contains("please sign in again") || (lower.contains("401") && lower.contains("unauthorized")) } private static func looksInternalTransport(lower: String) -> Bool { lower.contains("codex connection failed") || lower.contains("failed to fetch codex rate limits") || lower.contains("/backend-api/") || lower.contains("content-type=") || lower.contains("body={") || lower.contains("body=") || lower.contains("get https://") || lower.contains("get http://") || lower.contains("returned invalid data") } private static func looksOpenAIWebTimeout(lower: String) -> Bool { lower.contains("nsurlerrordomain") && (lower.contains("timed out") || lower.contains("error -1001")) } private static func looksOpenAIWebNetworkError(lower: String) -> Bool { lower.contains("nsurlerrordomain") } } struct CodexConsumerProjection { enum Surface { case liveCard case overrideCard case widget case menuBar } enum RateLane: String { case session case weekly } enum SupplementalMetric: String { case codeReview } struct PlanUtilizationLane { let role: PlanUtilizationSeriesName let window: RateWindow } enum DashboardVisibility { case hidden case displayOnly case attached } struct CreditsProjection { let snapshot: CreditsSnapshot? let userFacingError: String? var remaining: Double? { self.snapshot?.codexCreditLimit?.remaining ?? self.snapshot?.remaining } } struct UserFacingErrors { let usage: String? let credits: String? let dashboard: String? } struct Context { let snapshot: UsageSnapshot? let rawUsageError: String? let liveCredits: CreditsSnapshot? let rawCreditsError: String? let liveDashboard: OpenAIDashboardSnapshot? let rawDashboardError: String? let dashboardAttachmentAuthorized: Bool let dashboardRequiresLogin: Bool let now: Date } enum MenuBarFallback { case none case creditsBalance } let visibleRateLanes: [RateLane] let supplementalMetrics: [SupplementalMetric] let planUtilizationLanes: [PlanUtilizationLane] let dashboardVisibility: DashboardVisibility let credits: CreditsProjection? let menuBarFallback: MenuBarFallback let userFacingErrors: UserFacingErrors let canShowBuyCredits: Bool let hasUsageBreakdown: Bool let hasCreditsHistory: Bool private let rateWindowsByLane: [RateLane: RateWindow] private let codeReviewRemainingPercent: Double? private let codeReviewLimit: RateWindow? static func make(surface: Surface, context: Context) -> CodexConsumerProjection { let allowsLiveAdjuncts = surface != .overrideCard let dashboardVisibility = self.dashboardVisibility(surface: surface, context: context) let dashboard = allowsLiveAdjuncts && dashboardVisibility != .hidden ? context.liveDashboard : nil let rateWindowsByLane = self.rateWindowsByLane(snapshot: context.snapshot) let visibleRateLanes = self.visibleRateLanes(from: rateWindowsByLane, snapshot: context.snapshot) let planUtilizationLanes = self.planUtilizationLanes(from: rateWindowsByLane) let creditsProjection: CreditsProjection? = if allowsLiveAdjuncts, context.liveCredits != nil || context.rawCreditsError != nil { CreditsProjection( snapshot: context.liveCredits, userFacingError: CodexUIErrorMapper.userFacingMessage(context.rawCreditsError)) } else { nil } let userFacingErrors = UserFacingErrors( usage: CodexUIErrorMapper.userFacingMessage(context.rawUsageError), credits: allowsLiveAdjuncts ? CodexUIErrorMapper.userFacingMessage(context.rawCreditsError) : nil, dashboard: allowsLiveAdjuncts ? CodexUIErrorMapper.userFacingMessage(context.rawDashboardError) : nil) let supplementalMetrics: [SupplementalMetric] = if surface == .liveCard, dashboardVisibility == .attached, dashboard?.codeReviewRemainingPercent != nil { [.codeReview] } else { [] } let displayableUsageBreakdown = OpenAIDashboardDailyBreakdown.removingSkillUsageServices( from: dashboard?.usageBreakdown ?? []) let canShowBuyCredits = surface == .liveCard let hasUsageBreakdown = surface == .liveCard && dashboardVisibility == .attached && !displayableUsageBreakdown.isEmpty let hasCreditsHistory = surface == .liveCard && dashboardVisibility == .attached && !(dashboard?.dailyBreakdown ?? []).isEmpty return CodexConsumerProjection( visibleRateLanes: visibleRateLanes, supplementalMetrics: supplementalMetrics, planUtilizationLanes: planUtilizationLanes, dashboardVisibility: dashboardVisibility, credits: creditsProjection, menuBarFallback: self.menuBarFallback( creditsRemaining: creditsProjection?.remaining, rateWindowsByLane: rateWindowsByLane), userFacingErrors: userFacingErrors, canShowBuyCredits: canShowBuyCredits, hasUsageBreakdown: hasUsageBreakdown, hasCreditsHistory: hasCreditsHistory, rateWindowsByLane: rateWindowsByLane, codeReviewRemainingPercent: dashboardVisibility == .attached ? dashboard?.codeReviewRemainingPercent : nil, codeReviewLimit: dashboardVisibility == .attached ? dashboard?.codeReviewLimit : nil) } func rateWindow(for lane: RateLane) -> RateWindow? { self.rateWindowsByLane[lane] } func remainingPercent(for metric: SupplementalMetric) -> Double? { switch metric { case .codeReview: self.codeReviewRemainingPercent } } func limitWindow(for metric: SupplementalMetric) -> RateWindow? { switch metric { case .codeReview: self.codeReviewLimit } } private static func dashboardVisibility(surface: Surface, context: Context) -> DashboardVisibility { guard surface != .overrideCard else { return .hidden } guard context.dashboardRequiresLogin == false, context.liveDashboard != nil else { return .hidden } return context.dashboardAttachmentAuthorized ? .attached : .displayOnly } private static func rateWindowsByLane(snapshot: UsageSnapshot?) -> [RateLane: RateWindow] { guard let snapshot else { return [:] } var windowsByLane: [RateLane: RateWindow] = [:] let slottedWindows: [(RateLane, RateWindow)] = [ self.classifyRateWindow(snapshot.primary, slot: .primary), self.classifyRateWindow(snapshot.secondary, slot: .secondary), ].compactMap(\.self) for (lane, window) in slottedWindows { windowsByLane[lane] = window } return windowsByLane } private static func visibleRateLanes( from rateWindowsByLane: [RateLane: RateWindow], snapshot: UsageSnapshot?) -> [RateLane] { guard let snapshot else { return [] } let slottedLanes = [ self.classifyRateWindow(snapshot.primary, slot: .primary)?.0, self.classifyRateWindow(snapshot.secondary, slot: .secondary)?.0, ].compactMap(\.self) var visible: [RateLane] = [] for lane in slottedLanes where rateWindowsByLane[lane] != nil && !visible.contains(lane) { visible.append(lane) } return visible } private static func planUtilizationLanes(from rateWindowsByLane: [RateLane: RateWindow]) -> [PlanUtilizationLane] { let semanticOrder: [RateLane] = [.session, .weekly] return semanticOrder.compactMap { lane in guard let window = rateWindowsByLane[lane] else { return nil } return PlanUtilizationLane(role: self.planUtilizationRole(for: lane), window: window) } } private static func planUtilizationRole(for lane: RateLane) -> PlanUtilizationSeriesName { switch lane { case .session: .session case .weekly: .weekly } } private enum SnapshotSlot { case primary case secondary } private static func classifyRateWindow(_ window: RateWindow?, slot: SnapshotSlot) -> (RateLane, RateWindow)? { guard let window else { return nil } let lane: RateLane = switch window.windowMinutes { case 300: .session case 10080: .weekly default: switch slot { case .primary: .session case .secondary: .weekly } } return (lane, window) } private static func menuBarFallback( creditsRemaining: Double?, rateWindowsByLane: [RateLane: RateWindow]) -> MenuBarFallback { guard let creditsRemaining, creditsRemaining > 0 else { return .none } let hasExhaustedLane = rateWindowsByLane.values.contains { $0.remainingPercent <= 0 } let hasNoRateWindows = rateWindowsByLane.isEmpty return (hasExhaustedLane || hasNoRateWindows) ? .creditsBalance : .none } var hasExhaustedRateLane: Bool { self.rateWindowsByLane.values.contains { $0.remainingPercent <= 0 } } } extension UsageStore { func codexConsumerProjectionIfNeeded( for provider: UsageProvider, surface: CodexConsumerProjection.Surface, snapshotOverride: UsageSnapshot? = nil, errorOverride: String? = nil, now: Date = Date()) -> CodexConsumerProjection? { guard provider == .codex else { return nil } return self.codexConsumerProjection( surface: surface, snapshotOverride: snapshotOverride, errorOverride: errorOverride, now: now) } func codexConsumerProjection( surface: CodexConsumerProjection.Surface, snapshotOverride: UsageSnapshot? = nil, errorOverride: String? = nil, now: Date = Date()) -> CodexConsumerProjection { let snapshot = surface == .overrideCard ? snapshotOverride : snapshotOverride ?? self.snapshots[.codex] let rawUsageError = surface == .overrideCard ? errorOverride : errorOverride ?? self.errors[.codex] let context = CodexConsumerProjection.Context( snapshot: snapshot, rawUsageError: rawUsageError, liveCredits: self.credits, rawCreditsError: self.lastCreditsError, liveDashboard: self.openAIDashboard, rawDashboardError: self.lastOpenAIDashboardError, dashboardAttachmentAuthorized: self.openAIDashboardAttachmentAuthorized, dashboardRequiresLogin: self.openAIDashboardRequiresLogin, now: now) return CodexConsumerProjection.make(surface: surface, context: context) } func codexMenuBarCreditsRemaining(snapshotOverride: UsageSnapshot? = nil, now: Date = Date()) -> Double? { let projection = self.codexConsumerProjection( surface: .menuBar, snapshotOverride: snapshotOverride, now: now) guard projection.menuBarFallback == .creditsBalance else { return nil } return projection.credits?.remaining } }