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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
### Menu & Menu Bar
- Menu: opening OpenAI web submenus triggers a refresh when the data is stale.
- Menu: fix usage line labels to honor “Show usage as used”.
- Menu: tighten absolute reset timestamps and move the toggle into Display settings.
- Debug: add a toggle to keep Codex/Claude CLI sessions alive between probes.
- Debug: add a button to reset CLI probe sessions.
- App icon: use the classic icon on macOS 15 and earlier while keeping Liquid Glass for macOS 26+ (#178). Thanks @zerone0x!
Expand Down
8 changes: 7 additions & 1 deletion Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,7 @@ extension UsageMenuCardView.Model {
let paceDetail = Self.weeklyPaceDetail(
provider: input.provider,
window: weekly,
resetStyle: input.resetTimeDisplayStyle,
now: input.now,
showUsed: input.usageBarsShowUsed)
metrics.append(Metric(
Expand Down Expand Up @@ -842,10 +843,15 @@ extension UsageMenuCardView.Model {
private static func weeklyPaceDetail(
provider: UsageProvider,
window: RateWindow,
resetStyle: ResetTimeDisplayStyle,
now: Date,
showUsed: Bool) -> PaceDetail?
{
guard let detail = UsagePaceText.weeklyDetail(provider: provider, window: window, now: now) else { return nil }
guard let detail = UsagePaceText.weeklyDetail(
provider: provider,
window: window,
resetStyle: resetStyle,
now: now) else { return nil }
let expectedUsed = detail.expectedUsedPercent
let actualUsed = window.usedPercent
let expectedPercent = showUsed ? expectedUsed : (100 - expectedUsed)
Expand Down
5 changes: 4 additions & 1 deletion Sources/CodexBar/MenuDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ struct MenuDescriptor {
window: weekly,
resetStyle: resetStyle,
showUsed: settings.usageBarsShowUsed)
if let paceSummary = UsagePaceText.weeklySummary(provider: provider, window: weekly) {
if let paceSummary = UsagePaceText.weeklySummary(
provider: provider,
window: weekly,
resetStyle: resetStyle) {
entries.append(.text(paceSummary, .secondary))
}
}
Expand Down
2 changes: 0 additions & 2 deletions Sources/CodexBar/PreferencesAdvancedPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@ struct AdvancedPane: View {
.foregroundStyle(.tertiary)
}

Divider()

SettingsSection(contentSpacing: 10) {
PreferenceToggleRow(
title: "Show Debug Settings",
Expand Down
4 changes: 2 additions & 2 deletions Sources/CodexBar/PreferencesDisplayPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ struct DisplayPane: View {
subtitle: "Progress bars fill as you consume quota (instead of showing remaining).",
binding: self.$settings.usageBarsShowUsed)
PreferenceToggleRow(
title: "Show reset time as clock",
subtitle: "Display reset times as absolute clock values instead of countdowns.",
title: "Show reset date/time",
subtitle: "Display reset times as absolute date + time values instead of countdowns.",
binding: self.$settings.resetTimesShowAbsolute)
PreferenceToggleRow(
title: "Show credits + extra usage",
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/StatusItemController+Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -972,7 +972,7 @@ extension StatusItemController {
}
if let resetTime = timeLimit.nextResetTime {
let reset = self.settings.resetTimeDisplayStyle == .absolute
? UsageFormatter.resetDescription(from: resetTime)
? UsageFormatter.resetAbsoluteDescription(from: resetTime)
: UsageFormatter.resetCountdownDescription(from: resetTime)
let item = NSMenuItem(title: "Resets: \(reset)", action: nil, keyEquivalent: "")
item.isEnabled = false
Expand Down
35 changes: 26 additions & 9 deletions Sources/CodexBar/UsagePaceText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,30 @@ enum UsagePaceText {

private static let minimumExpectedPercent: Double = 3

static func weeklySummary(provider: UsageProvider, window: RateWindow, now: Date = .init()) -> String? {
guard let detail = weeklyDetail(provider: provider, window: window, now: now) else { return nil }
static func weeklySummary(
provider: UsageProvider,
window: RateWindow,
resetStyle: ResetTimeDisplayStyle = .countdown,
now: Date = .init()) -> String?
{
guard let detail = weeklyDetail(provider: provider, window: window, resetStyle: resetStyle, now: now)
else { return nil }
if let rightLabel = detail.rightLabel {
return "Pace: \(detail.leftLabel) · \(rightLabel)"
}
return "Pace: \(detail.leftLabel)"
}

static func weeklyDetail(provider: UsageProvider, window: RateWindow, now: Date = .init()) -> WeeklyDetail? {
static func weeklyDetail(
provider: UsageProvider,
window: RateWindow,
resetStyle: ResetTimeDisplayStyle = .countdown,
now: Date = .init()) -> WeeklyDetail?
{
guard let pace = weeklyPace(provider: provider, window: window, now: now) else { return nil }
return WeeklyDetail(
leftLabel: Self.detailLeftLabel(for: pace),
rightLabel: Self.detailRightLabel(for: pace, now: now),
rightLabel: Self.detailRightLabel(for: pace, now: now, resetStyle: resetStyle),
expectedUsedPercent: pace.expectedUsedPercent,
stage: pace.stage)
}
Expand All @@ -40,16 +51,22 @@ enum UsagePaceText {
}
}

private static func detailRightLabel(for pace: UsagePace, now: Date) -> String? {
private static func detailRightLabel(for pace: UsagePace, now: Date, resetStyle: ResetTimeDisplayStyle) -> String? {
if pace.willLastToReset { return "Lasts until reset" }
guard let etaSeconds = pace.etaSeconds else { return nil }
let etaText = Self.durationText(seconds: etaSeconds, now: now)
if etaText == "now" { return "Runs out now" }
return "Runs out in \(etaText)"
let etaText = Self.durationText(seconds: etaSeconds, now: now, resetStyle: resetStyle)
if resetStyle == .countdown {
if etaText == "now" { return "Runs out now" }
return "Runs out in \(etaText)"
}
return "Runs out at \(etaText)"
}

private static func durationText(seconds: TimeInterval, now: Date) -> String {
private static func durationText(seconds: TimeInterval, now: Date, resetStyle: ResetTimeDisplayStyle) -> String {
let date = now.addingTimeInterval(seconds)
if resetStyle == .absolute {
return UsageFormatter.resetAbsoluteDescription(from: date, now: now)
}
let countdown = UsageFormatter.resetCountdownDescription(from: date, now: now)
if countdown == "now" { return "now" }
if countdown.hasPrefix("in ") { return String(countdown.dropFirst(3)) }
Expand Down
18 changes: 16 additions & 2 deletions Sources/CodexBarCore/UsageFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ public enum UsageFormatter {
return date.formatted(date: .abbreviated, time: .shortened)
}

public static func resetAbsoluteDescription(from date: Date, now: Date = .init()) -> String {
let calendar = Calendar.current
if calendar.isDate(date, inSameDayAs: now) {
return date.formatted(date: .omitted, time: .shortened)
}
if let weekAhead = calendar.date(byAdding: .day, value: 7, to: now), date < weekAhead {
let weekday = date.formatted(.dateTime.weekday(.abbreviated))
let time = date.formatted(date: .omitted, time: .shortened)
return "\(weekday) \(time)"
}
return date.formatted(.dateTime.month(.abbreviated).day().hour().minute())
}

public static func resetLine(
for window: RateWindow,
style: ResetTimeDisplayStyle,
Expand All @@ -55,8 +68,9 @@ public enum UsageFormatter {
if let date = window.resetsAt {
let text = style == .countdown
? self.resetCountdownDescription(from: date, now: now)
: self.resetDescription(from: date, now: now)
return "Resets \(text)"
: self.resetAbsoluteDescription(from: date, now: now)
let prefix = style == .countdown ? "Resets " : "Resets at "
return "\(prefix)\(text)"
}

if let desc = window.resetDescription {
Expand Down
20 changes: 20 additions & 0 deletions Tests/CodexBarTests/UsageFormatterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,26 @@ struct UsageFormatterTests {
#expect(text == "Resets in 11m")
}

@Test
func resetLineUsesAbsoluteWhenResetsAtIsAvailable() {
let now = Date(timeIntervalSince1970: 1_000_000)
let reset = now.addingTimeInterval(60 * 60)
let window = RateWindow(usedPercent: 0, windowMinutes: nil, resetsAt: reset, resetDescription: nil)
let text = UsageFormatter.resetLine(for: window, style: .absolute, now: now)
#expect(text?.hasPrefix("Resets at ") == true)
}

@Test
func resetAbsoluteDescriptionOmitsYear() {
let now = Date(timeIntervalSince1970: 1_785_000_000) // Jan 2026-ish
let sameDay = now.addingTimeInterval(3 * 3600)
let later = now.addingTimeInterval(15 * 24 * 3600)
let sameDayText = UsageFormatter.resetAbsoluteDescription(from: sameDay, now: now)
let laterText = UsageFormatter.resetAbsoluteDescription(from: later, now: now)
#expect(!sameDayText.contains("2026"))
#expect(!laterText.contains("2026"))
}

@Test
func resetLineFallsBackToProvidedDescription() {
let window = RateWindow(
Expand Down
14 changes: 14 additions & 0 deletions Tests/CodexBarTests/UsagePaceTextTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ struct UsagePaceTextTests {
#expect(summary == "Pace: 7% in deficit · Runs out in 3d")
}

@Test
func weeklyPaceDetail_usesAbsoluteResetStyle() {
let now = Date(timeIntervalSince1970: 0)
let window = RateWindow(
usedPercent: 50,
windowMinutes: 10080,
resetsAt: now.addingTimeInterval(4 * 24 * 3600),
resetDescription: nil)

let detail = UsagePaceText.weeklyDetail(provider: .codex, window: window, resetStyle: .absolute, now: now)

#expect(detail?.rightLabel?.hasPrefix("Runs out at ") == true)
}

@Test
func weeklyPaceDetail_hidesWhenResetIsMissing() {
let now = Date(timeIntervalSince1970: 0)
Expand Down
Loading