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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 8 additions & 1 deletion Sources/CodexBarCLI/CLIHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions Sources/CodexBarCore/OpenAIDashboardModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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],
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBarCore/OpenAIWeb/OpenAIDashboardFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -224,6 +225,7 @@ public struct OpenAIDashboardFetcher {
return OpenAIDashboardSnapshot(
signedInEmail: scrape.signedInEmail,
codeReviewRemainingPercent: codeReview,
codeReviewLimit: codeReviewLimit,
creditEvents: events,
dailyBreakdown: breakdown,
usageBreakdown: usageBreakdown,
Expand Down
71 changes: 47 additions & 24 deletions Sources/CodexBarCore/OpenAIWeb/OpenAIDashboardParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)? {
Expand Down Expand Up @@ -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") || lower.contains("core 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)
Expand Down
7 changes: 6 additions & 1 deletion Tests/CodexBarTests/CLIEntryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
Expand All @@ -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"))
}

Expand Down
12 changes: 12 additions & 0 deletions Tests/CodexBarTests/MenuCardModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
Expand Down Expand Up @@ -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: [],
Expand All @@ -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
Expand Down
26 changes: 26 additions & 0 deletions Tests/CodexBarTests/OpenAIDashboardParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,32 @@ 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 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"
Expand Down
Loading