From 952bd5816722f1b59f0d8254d882ffe4c6bd72c4 Mon Sep 17 00:00:00 2001 From: Qichen Liu Date: Fri, 20 Mar 2026 20:58:06 +0800 Subject: [PATCH 1/2] Add reset time display for Codex code review limit Co-Authored-By: Claude Opus 4.6 (1M context) --- Sources/CodexBar/MenuCardView.swift | 5 +- Sources/CodexBarCLI/CLIHelpers.swift | 9 ++- .../CodexBarCore/OpenAIDashboardModels.swift | 5 ++ .../OpenAIWeb/OpenAIDashboardFetcher.swift | 2 + .../OpenAIWeb/OpenAIDashboardParser.swift | 71 ++++++++++++------- Tests/CodexBarTests/CLIEntryTests.swift | 7 +- Tests/CodexBarTests/MenuCardModelTests.swift | 12 ++++ .../OpenAIDashboardParserTests.swift | 13 ++++ 8 files changed, 97 insertions(+), 27 deletions(-) diff --git a/Sources/CodexBar/MenuCardView.swift b/Sources/CodexBar/MenuCardView.swift index 34ac51846..19efd1b4a 100644 --- a/Sources/CodexBar/MenuCardView.swift +++ b/Sources/CodexBar/MenuCardView.swift @@ -998,12 +998,15 @@ extension UsageMenuCardView.Model { if input.provider == .codex, let remaining = input.dashboard?.codeReviewRemainingPercent { let percent = input.usageBarsShowUsed ? (100 - remaining) : remaining + let resetText = input.dashboard?.codeReviewLimit.flatMap { + Self.resetText(for: $0, style: input.resetTimeDisplayStyle, now: input.now) + } metrics.append(Metric( id: "code-review", title: "Code review", percent: Self.clamped(percent), percentStyle: percentStyle, - resetText: nil, + resetText: resetText, detailText: nil, detailLeftText: nil, detailRightText: nil, diff --git a/Sources/CodexBarCLI/CLIHelpers.swift b/Sources/CodexBarCLI/CLIHelpers.swift index 4edfd7c64..ec792fb3b 100644 --- a/Sources/CodexBarCLI/CLIHelpers.swift +++ b/Sources/CodexBarCLI/CLIHelpers.swift @@ -206,6 +206,7 @@ extension CodexBarCLI { return OpenAIDashboardSnapshot( signedInEmail: cache.snapshot.signedInEmail, codeReviewRemainingPercent: cache.snapshot.codeReviewRemainingPercent, + codeReviewLimit: cache.snapshot.codeReviewLimit, creditEvents: cache.snapshot.creditEvents, dailyBreakdown: OpenAIDashboardSnapshot.makeDailyBreakdown( from: cache.snapshot.creditEvents, @@ -239,7 +240,13 @@ extension CodexBarCLI { } if let remaining = dash.codeReviewRemainingPercent { let percent = Int(remaining.rounded()) - lines.append("Code review: \(percent)% remaining") + if let limit = dash.codeReviewLimit, + let reset = UsageFormatter.resetLine(for: limit, style: .countdown) + { + lines.append("Code review: \(percent)% remaining (\(reset))") + } else { + lines.append("Code review: \(percent)% remaining") + } } if let first = dash.creditEvents.first { let day = first.date.formatted(date: .abbreviated, time: .omitted) diff --git a/Sources/CodexBarCore/OpenAIDashboardModels.swift b/Sources/CodexBarCore/OpenAIDashboardModels.swift index 367223a96..e4fe3a06f 100644 --- a/Sources/CodexBarCore/OpenAIDashboardModels.swift +++ b/Sources/CodexBarCore/OpenAIDashboardModels.swift @@ -3,6 +3,7 @@ import Foundation public struct OpenAIDashboardSnapshot: Codable, Equatable, Sendable { public let signedInEmail: String? public let codeReviewRemainingPercent: Double? + public let codeReviewLimit: RateWindow? public let creditEvents: [CreditEvent] public let dailyBreakdown: [OpenAIDashboardDailyBreakdown] /// Usage breakdown time series from the Codex dashboard chart ("Usage breakdown", 30 days). @@ -19,6 +20,7 @@ public struct OpenAIDashboardSnapshot: Codable, Equatable, Sendable { public init( signedInEmail: String?, codeReviewRemainingPercent: Double?, + codeReviewLimit: RateWindow? = nil, creditEvents: [CreditEvent], dailyBreakdown: [OpenAIDashboardDailyBreakdown], usageBreakdown: [OpenAIDashboardDailyBreakdown], @@ -31,6 +33,7 @@ public struct OpenAIDashboardSnapshot: Codable, Equatable, Sendable { { self.signedInEmail = signedInEmail self.codeReviewRemainingPercent = codeReviewRemainingPercent + self.codeReviewLimit = codeReviewLimit self.creditEvents = creditEvents self.dailyBreakdown = dailyBreakdown self.usageBreakdown = usageBreakdown @@ -45,6 +48,7 @@ public struct OpenAIDashboardSnapshot: Codable, Equatable, Sendable { private enum CodingKeys: String, CodingKey { case signedInEmail case codeReviewRemainingPercent + case codeReviewLimit case creditEvents case dailyBreakdown case usageBreakdown @@ -62,6 +66,7 @@ public struct OpenAIDashboardSnapshot: Codable, Equatable, Sendable { self.codeReviewRemainingPercent = try container.decodeIfPresent( Double.self, forKey: .codeReviewRemainingPercent) + self.codeReviewLimit = try container.decodeIfPresent(RateWindow.self, forKey: .codeReviewLimit) self.creditEvents = try container.decodeIfPresent([CreditEvent].self, forKey: .creditEvents) ?? [] self.dailyBreakdown = try container.decodeIfPresent( [OpenAIDashboardDailyBreakdown].self, diff --git a/Sources/CodexBarCore/OpenAIWeb/OpenAIDashboardFetcher.swift b/Sources/CodexBarCore/OpenAIWeb/OpenAIDashboardFetcher.swift index dba3a36fe..f9dfe030d 100644 --- a/Sources/CodexBarCore/OpenAIWeb/OpenAIDashboardFetcher.swift +++ b/Sources/CodexBarCore/OpenAIWeb/OpenAIDashboardFetcher.swift @@ -156,6 +156,7 @@ public struct OpenAIDashboardFetcher { let breakdown = OpenAIDashboardSnapshot.makeDailyBreakdown(from: events, maxDays: 30) let usageBreakdown = scrape.usageBreakdown let rateLimits = OpenAIDashboardParser.parseRateLimits(bodyText: bodyText) + let codeReviewLimit = OpenAIDashboardParser.parseCodeReviewLimit(bodyText: bodyText) let creditsRemaining = OpenAIDashboardParser.parseCreditsRemaining(bodyText: bodyText) let accountPlan = scrape.bodyHTML.flatMap(OpenAIDashboardParser.parsePlanFromHTML) let hasUsageLimits = rateLimits.primary != nil || rateLimits.secondary != nil @@ -224,6 +225,7 @@ public struct OpenAIDashboardFetcher { return OpenAIDashboardSnapshot( signedInEmail: scrape.signedInEmail, codeReviewRemainingPercent: codeReview, + codeReviewLimit: codeReviewLimit, creditEvents: events, dailyBreakdown: breakdown, usageBreakdown: usageBreakdown, diff --git a/Sources/CodexBarCore/OpenAIWeb/OpenAIDashboardParser.swift b/Sources/CodexBarCore/OpenAIWeb/OpenAIDashboardParser.swift index 88f9373e3..890d1add8 100644 --- a/Sources/CodexBarCore/OpenAIWeb/OpenAIDashboardParser.swift +++ b/Sources/CodexBarCore/OpenAIWeb/OpenAIDashboardParser.swift @@ -109,6 +109,20 @@ public enum OpenAIDashboardParser { return (primary, secondary) } + public static func parseCodeReviewLimit(bodyText: String, now: Date = .init()) -> RateWindow? { + let cleaned = bodyText.replacingOccurrences(of: "\r", with: "\n") + let lines = cleaned + .split(whereSeparator: \.isNewline) + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + + return self.parseRateWindow( + lines: lines, + match: self.isCodeReviewLimitLine, + windowMinutes: nil, + now: now) + } + public static func parsePlanFromHTML(html: String) -> String? { if let data = self.clientBootstrapJSONData(fromHTML: html), let plan = self.findPlan(in: data) @@ -233,36 +247,38 @@ public enum OpenAIDashboardParser { private static func parseRateWindow( lines: [String], match: (String) -> Bool, - windowMinutes: Int, + windowMinutes: Int?, now: Date) -> RateWindow? { - guard let idx = lines.firstIndex(where: match) else { return nil } - let end = min(lines.count - 1, idx + 5) - let windowLines = Array(lines[idx...end]) - - var percentValue: Double? - var isRemaining = true - for line in windowLines { - if let percent = self.parsePercent(from: line) { - percentValue = percent.value - isRemaining = percent.isRemaining - break + for idx in lines.indices where match(lines[idx]) { + let end = min(lines.count - 1, idx + 5) + let windowLines = Array(lines[idx...end]) + + var percentValue: Double? + var isRemaining = true + for line in windowLines { + if let percent = self.parsePercent(from: line) { + percentValue = percent.value + isRemaining = percent.isRemaining + break + } } - } - guard let percentValue else { return nil } - let usedPercent = isRemaining ? max(0, min(100, 100 - percentValue)) : max(0, min(100, percentValue)) + guard let percentValue else { continue } + let usedPercent = isRemaining ? max(0, min(100, 100 - percentValue)) : max(0, min(100, percentValue)) - let resetLine = windowLines.first { $0.localizedCaseInsensitiveContains("reset") } - let resetDescription = resetLine?.trimmingCharacters(in: .whitespacesAndNewlines) - let resetsAt = resetLine.flatMap { self.parseResetDate(from: $0, now: now) } - let fallbackDescription = resetsAt.map { UsageFormatter.resetDescription(from: $0) } + let resetLine = windowLines.first { $0.localizedCaseInsensitiveContains("reset") } + let resetDescription = resetLine?.trimmingCharacters(in: .whitespacesAndNewlines) + let resetsAt = resetLine.flatMap { self.parseResetDate(from: $0, now: now) } + let fallbackDescription = resetsAt.map { UsageFormatter.resetDescription(from: $0) } - return RateWindow( - usedPercent: usedPercent, - windowMinutes: windowMinutes, - resetsAt: resetsAt, - resetDescription: resetDescription ?? fallbackDescription) + return RateWindow( + usedPercent: usedPercent, + windowMinutes: windowMinutes, + resetsAt: resetsAt, + resetDescription: resetDescription ?? fallbackDescription) + } + return nil } private static func parsePercent(from line: String) -> (value: Double, isRemaining: Bool)? { @@ -292,6 +308,13 @@ public enum OpenAIDashboardParser { return false } + private static func isCodeReviewLimitLine(_ line: String) -> Bool { + let lower = line.lowercased() + guard lower.contains("code review") else { return false } + if lower.contains("github code review") { return false } + return true + } + private static func parseResetDate(from line: String, now: Date) -> Date? { var raw = line.trimmingCharacters(in: .whitespacesAndNewlines) raw = raw.replacingOccurrences(of: #"(?i)^resets?:?\s*"#, with: "", options: .regularExpression) diff --git a/Tests/CodexBarTests/CLIEntryTests.swift b/Tests/CodexBarTests/CLIEntryTests.swift index fe90d3b39..daa8f174f 100644 --- a/Tests/CodexBarTests/CLIEntryTests.swift +++ b/Tests/CodexBarTests/CLIEntryTests.swift @@ -53,6 +53,11 @@ struct CLIEntryTests { let snapshot = OpenAIDashboardSnapshot( signedInEmail: "user@example.com", codeReviewRemainingPercent: 45, + codeReviewLimit: RateWindow( + usedPercent: 55, + windowMinutes: nil, + resetsAt: Date().addingTimeInterval(3600), + resetDescription: nil), creditEvents: [event], dailyBreakdown: [], usageBreakdown: [], @@ -62,7 +67,7 @@ struct CLIEntryTests { let text = CodexBarCLI.renderOpenAIWebDashboardText(snapshot) #expect(text.contains("Web session: user@example.com")) - #expect(text.contains("Code review: 45% remaining")) + #expect(text.contains("Code review: 45% remaining (Resets in ")) #expect(text.contains("Web history: 1 events")) } diff --git a/Tests/CodexBarTests/MenuCardModelTests.swift b/Tests/CodexBarTests/MenuCardModelTests.swift index 052bd0dad..60eddeb6a 100644 --- a/Tests/CodexBarTests/MenuCardModelTests.swift +++ b/Tests/CodexBarTests/MenuCardModelTests.swift @@ -93,6 +93,11 @@ struct MenuCardModelTests { let dashboard = OpenAIDashboardSnapshot( signedInEmail: "codex@example.com", codeReviewRemainingPercent: 73, + codeReviewLimit: RateWindow( + usedPercent: 27, + windowMinutes: nil, + resetsAt: now.addingTimeInterval(3600), + resetDescription: nil), creditEvents: [], dailyBreakdown: [], usageBreakdown: [], @@ -144,6 +149,11 @@ struct MenuCardModelTests { let dashboard = OpenAIDashboardSnapshot( signedInEmail: "codex@example.com", codeReviewRemainingPercent: 73, + codeReviewLimit: RateWindow( + usedPercent: 27, + windowMinutes: nil, + resetsAt: now.addingTimeInterval(3600), + resetDescription: nil), creditEvents: [], dailyBreakdown: [], usageBreakdown: [], @@ -170,6 +180,8 @@ struct MenuCardModelTests { now: now)) #expect(model.metrics.contains { $0.title == "Code review" && $0.percent == 73 }) + let codeReviewMetric = model.metrics.first { $0.id == "code-review" } + #expect(codeReviewMetric?.resetText?.contains("Resets") == true) } @Test diff --git a/Tests/CodexBarTests/OpenAIDashboardParserTests.swift b/Tests/CodexBarTests/OpenAIDashboardParserTests.swift index 464ed0e6f..6254a157b 100644 --- a/Tests/CodexBarTests/OpenAIDashboardParserTests.swift +++ b/Tests/CodexBarTests/OpenAIDashboardParserTests.swift @@ -32,6 +32,19 @@ struct OpenAIDashboardParserTests { #expect(OpenAIDashboardParser.parseCodeReviewRemainingPercent(bodyText: body) == 100) } + @Test + func `parses code review limit with reset`() { + let body = """ + Balance + Code review + 42% remaining + Resets tomorrow at 2:15 PM + """ + let limit = OpenAIDashboardParser.parseCodeReviewLimit(bodyText: body) + #expect(abs((limit?.usedPercent ?? 0) - 58) < 0.001) + #expect(limit?.resetDescription?.lowercased().contains("resets") == true) + } + @Test func `parses credits remaining`() { let body = "Balance\nCredits remaining 1,234.56\nUsage" From 178ff6e1aa05526a789d6666f6bb41bd93649972 Mon Sep 17 00:00:00 2001 From: Qichen Liu Date: Sat, 21 Mar 2026 19:57:27 +0800 Subject: [PATCH 2/2] fix(parser): support Core review in code review reset matcher --- .../OpenAIWeb/OpenAIDashboardParser.swift | 2 +- .../CodexBarTests/OpenAIDashboardParserTests.swift | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Sources/CodexBarCore/OpenAIWeb/OpenAIDashboardParser.swift b/Sources/CodexBarCore/OpenAIWeb/OpenAIDashboardParser.swift index 890d1add8..a2a3d914c 100644 --- a/Sources/CodexBarCore/OpenAIWeb/OpenAIDashboardParser.swift +++ b/Sources/CodexBarCore/OpenAIWeb/OpenAIDashboardParser.swift @@ -310,7 +310,7 @@ public enum OpenAIDashboardParser { private static func isCodeReviewLimitLine(_ line: String) -> Bool { let lower = line.lowercased() - guard lower.contains("code review") else { return false } + guard lower.contains("code review") || lower.contains("core review") else { return false } if lower.contains("github code review") { return false } return true } diff --git a/Tests/CodexBarTests/OpenAIDashboardParserTests.swift b/Tests/CodexBarTests/OpenAIDashboardParserTests.swift index 6254a157b..7379dfe9f 100644 --- a/Tests/CodexBarTests/OpenAIDashboardParserTests.swift +++ b/Tests/CodexBarTests/OpenAIDashboardParserTests.swift @@ -45,6 +45,19 @@ struct OpenAIDashboardParserTests { #expect(limit?.resetDescription?.lowercased().contains("resets") == true) } + @Test + func `parses core review limit with reset`() { + let body = """ + Balance + Core review + 42% remaining + Resets tomorrow at 2:15 PM + """ + let limit = OpenAIDashboardParser.parseCodeReviewLimit(bodyText: body) + #expect(abs((limit?.usedPercent ?? 0) - 58) < 0.001) + #expect(limit?.resetDescription?.lowercased().contains("resets") == true) + } + @Test func `parses credits remaining`() { let body = "Balance\nCredits remaining 1,234.56\nUsage"