Skip to content
Closed
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 @@ -13,6 +13,7 @@
- Menu bar: reserve quota-bar space consistently across Overview and provider switcher segments so selection no longer changes segment height (#1445). Thanks @Zihao-Qi!
- Cost usage: accept normal models.dev catalog churn while retaining prior model prices as fallbacks, so newly priced models appear without requiring a manual cache reset (#1438). Thanks @tom-rigelblu!
- Menu bar: detect Tahoe Control Center proxy windows parked in the blocked offscreen slot during startup recovery, so hidden icons show the existing guidance without weakening menu-bar-manager safeguards (#1440).
- Weekly pace: use configured work days for standard weekly pace calculations while leaving historical Codex pacing unchanged (#1357, fixes #1356). Thanks @pstanton237!
- Menu bar: anchor merged provider dropdowns to the status item's trailing edge without marking preserved in-flight refresh content fresh, preventing horizontal drift while keeping deferred updates visible (#1288). Thanks @Yuxin-Qiao!
- Antigravity: fall back to the CLI usage server when the desktop app is closed, keep helper sessions owned and bounded without hidden sign-in flows, and show model rows with missing usage as unavailable instead of exhausted (#1313). Thanks @enieuwy!
- Cost usage: replace repeated Foundation metadata/root checks with one portable file-stat pass so expired Codex history refreshes stay responsive on very large session archives (#1392). Thanks @TheAngryPit and @ProspectOre!
Expand Down
12 changes: 10 additions & 2 deletions Sources/CodexBar/MenuCardView+ModelHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,11 @@ extension UsageMenuCardView.Model {
guard input.provider == .cursor,
window.windowMinutes != nil
else { return nil }
let resolved = pace ?? UsagePace.weekly(window: window, now: input.now, defaultWindowMinutes: 10080)
let resolved = pace ?? UsagePace.weekly(
window: window,
now: input.now,
defaultWindowMinutes: 10080,
workDays: input.workDaysPerWeek)
guard let resolved,
resolved.expectedUsedPercent >= 3
else { return nil }
Expand Down Expand Up @@ -255,7 +259,11 @@ extension UsageMenuCardView.Model {
now: input.now,
showUsed: input.usageBarsShowUsed)
case 10080:
let pace = UsagePace.weekly(window: window, now: input.now, defaultWindowMinutes: 10080)
let pace = UsagePace.weekly(
window: window,
now: input.now,
defaultWindowMinutes: 10080,
workDays: input.workDaysPerWeek)
.flatMap { $0.expectedUsedPercent >= 3 ? $0 : nil }
return Self.weeklyPaceDetail(
window: window,
Expand Down
6 changes: 5 additions & 1 deletion Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1505,7 +1505,11 @@ extension UsageMenuCardView.Model {
window: window,
now: input.now,
pace: input.weeklyPace
?? UsagePace.weekly(window: window, now: input.now, defaultWindowMinutes: 10080)
?? UsagePace.weekly(
window: window,
now: input.now,
defaultWindowMinutes: 10080,
workDays: input.workDaysPerWeek)
.flatMap { $0.expectedUsedPercent >= 3 ? $0 : nil },
showUsed: input.usageBarsShowUsed)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@
"show_quota_warning_markers_title" = "Show quota warning markers";
"show_quota_warning_markers_subtitle" = "Draw threshold tick marks on usage bars when quota warnings are configured.";
"weekly_progress_work_days_title" = "Weekly progress work days";
"weekly_progress_work_days_subtitle" = "Draw day-boundary tick marks on weekly usage bars.";
"weekly_progress_work_days_subtitle" = "Set work days for weekly usage-bar markers and pace calculations.";
"show_reset_time_as_clock_title" = "Show reset time as clock";
"show_reset_time_as_clock_subtitle" = "Display reset times as absolute clock values instead of countdowns.";
"show_provider_changelog_links_title" = "Show provider changelog links";
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/Resources/fr.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@
"show_quota_warning_markers_title" = "Afficher les marqueurs d'avertissement de quota";
"show_quota_warning_markers_subtitle" = "Dessinez des coches de seuil sur les barres d’utilisation lorsque des avertissements de quota sont configurés.";
"weekly_progress_work_days_title" = "Jours de travail hebdomadaires";
"weekly_progress_work_days_subtitle" = "Dessinez des graduations journalières sur les barres d’utilisation hebdomadaire.";
"weekly_progress_work_days_subtitle" = "Définit les jours ouvrés pour les repères des barres d’utilisation hebdomadaire et le calcul du rythme.";
"show_reset_time_as_clock_title" = "Afficher l'heure de réinitialisation sous forme d'horloge";
"show_reset_time_as_clock_subtitle" = "Affichez les temps de réinitialisation sous forme de valeurs d'horloge absolues au lieu de comptes à rebours.";
"show_provider_changelog_links_title" = "Afficher les liens du journal des modifications du fournisseur";
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/Resources/ja.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@
"show_quota_warning_markers_title" = "クォータ警告マーカーを表示";
"show_quota_warning_markers_subtitle" = "クォータ警告が設定されている場合、使用量バーにしきい値の目盛りを描画します。";
"weekly_progress_work_days_title" = "週間進捗の作業日";
"weekly_progress_work_days_subtitle" = "週間使用量バーに日付の区切り目盛りを描画します。";
"weekly_progress_work_days_subtitle" = "週間使用量バーの目盛りとペース計算に使用する作業日を設定します。";
"show_reset_time_as_clock_title" = "リセット時刻を時計表示";
"show_reset_time_as_clock_subtitle" = "リセット時刻をカウントダウンではなく絶対時刻で表示します。";
"show_provider_changelog_links_title" = "プロバイダの変更履歴リンクを表示";
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/Resources/nl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@
"show_quota_warning_markers_title" = "Toon waarschuwingsmarkeringen voor quota";
"show_quota_warning_markers_subtitle" = "Teken drempelmarkeringen op gebruiksbalken wanneer quotawaarschuwingen zijn geconfigureerd.";
"weekly_progress_work_days_title" = "Wekelijkse voortgang werkdagen";
"weekly_progress_work_days_subtitle" = "Teken daggrensmarkeringen op de wekelijkse gebruiksbalken.";
"weekly_progress_work_days_subtitle" = "Stel werkdagen in voor markeringen op wekelijkse gebruiksbalken en tempoberekeningen.";
"show_reset_time_as_clock_title" = "Toon resettijd als klok";
"show_reset_time_as_clock_subtitle" = "Geef resettijden weer als absolute klokwaarden in plaats van aftellingen.";
"show_provider_changelog_links_title" = "Toon provider changelog-links";
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@
"show_quota_warning_markers_title" = "Mostrar marcadores de alerta de cota";
"show_quota_warning_markers_subtitle" = "Desenha marcas de limite nas barras de uso quando os alertas de cota estão configurados.";
"weekly_progress_work_days_title" = "Dias úteis no progresso semanal";
"weekly_progress_work_days_subtitle" = "Desenha marcas de limite de dia nas barras de uso semanal.";
"weekly_progress_work_days_subtitle" = "Define os dias úteis para marcadores das barras de uso semanal e cálculos de ritmo.";
"show_reset_time_as_clock_title" = "Mostrar renovação como horário";
"show_reset_time_as_clock_subtitle" = "Mostra horários de renovação como horas absolutas, em vez de contagens regressivas.";
"show_provider_changelog_links_title" = "Mostrar links de changelog dos provedores";
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/Resources/sv.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@
"show_quota_warning_markers_title" = "Visa kvotvarningsmarkörer";
"show_quota_warning_markers_subtitle" = "Rita tröskelmarkeringar på användningsstaplar när kvotvarningar är konfigurerade.";
"weekly_progress_work_days_title" = "Arbetsdagar i veckoförlopp";
"weekly_progress_work_days_subtitle" = "Rita dagsgränsmarkeringar på veckostaplar.";
"weekly_progress_work_days_subtitle" = "Ställ in arbetsdagar för markeringar i veckostaplar och tempoberäkningar.";
"show_reset_time_as_clock_title" = "Visa återställningstid som klockslag";
"show_reset_time_as_clock_subtitle" = "Visa återställningstider som klockslag i stället för nedräkningar.";
"show_provider_changelog_links_title" = "Visa länkar till leverantörers ändringsloggar";
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/Resources/uk.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@
"show_quota_warning_markers_title" = "Показати маркери попередження про квоти";
"show_quota_warning_markers_subtitle" = "Малюйте порогові позначки на панелях використання, коли налаштовано попередження про квоту.";
"weekly_progress_work_days_title" = "Щотижневі робочі дні";
"weekly_progress_work_days_subtitle" = "Позначте межі дня на смугах тижневого використання.";
"weekly_progress_work_days_subtitle" = "Задайте робочі дні для позначок на смугах тижневого використання та розрахунків темпу.";
"show_reset_time_as_clock_title" = "Показувати час скидання як годинник";
"show_reset_time_as_clock_subtitle" = "Відображення часу скидання як абсолютних значень годинника замість зворотного відліку.";
"show_provider_changelog_links_title" = "Показати посилання журналу змін провайдера";
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/Resources/vi.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@
"show_quota_warning_markers_title" = "Hiển thị Hạn mức dấu cảnh báo";
"show_quota_warning_markers_subtitle" = "Vẽ dấu kiểm ngưỡng trên thanh Mức sử dụng khi cảnh báo Hạn mức được định cấu hình.";
"weekly_progress_work_days_title" = "Tiến độ ngày làm việc hàng tuần";
"weekly_progress_work_days_subtitle" = "Vẽ các dấu kiểm ranh giới ngày trên các thanh Mức sử dụng hàng tuần.";
"weekly_progress_work_days_subtitle" = "Đặt ngày làm việc cho các vạch trên thanh sử dụng hằng tuần và phép tính nhịp độ.";
"show_reset_time_as_clock_title" = "Hiển thị Đặt lại thời gian dưới dạng đồng hồ";
"show_reset_time_as_clock_subtitle" = "Hiển thị Đặt lại thời gian dưới dạng giá trị đồng hồ tuyệt đối thay vì đếm ngược.";
"show_provider_changelog_links_title" = "Hiển thị Nhà cung cấp liên kết nhật ký thay đổi";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,7 @@
"Cleanup ideas" = "清理建议";
"%d unreadable item(s) skipped" = "已跳过 %d 个不可读项目";
"weekly_progress_work_days_title" = "工作日刻度线";
"weekly_progress_work_days_subtitle" = "在每周用量条上显示按天分隔的刻度线。";
"weekly_progress_work_days_subtitle" = "设置用于每周用量条刻度和进度计算的工作日。";
"copilot_device_code" = "设备代码已复制到剪贴板:%1$@\n\n请在以下地址验证:%2$@";
"copilot_waiting_text" = "请在浏览器中完成登录。\n登录完成后,此窗口会自动关闭。";
"vertex_ai_login_instructions" = "要跟踪 Vertex AI 用量,请通过 Google Cloud 进行认证。\n\n1. 打开终端\n2. 运行:gcloud auth application-default login\n3. 按照浏览器提示登录\n4. 设置你的项目:gcloud config set project PROJECT_ID\n\n是否现在打开终端?";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@
"show_quota_warning_markers_title" = "顯示配額提醒標記";
"show_quota_warning_markers_subtitle" = "設定配額提醒後,在使用量條上繪製門檻刻度標記。";
"weekly_progress_work_days_title" = "每週進度工作日標記";
"weekly_progress_work_days_subtitle" = "在每週使用量條上繪製日期邊界刻度標記。";
"weekly_progress_work_days_subtitle" = "設定用於每週用量條刻度與進度計算的工作日。";
"show_reset_time_as_clock_title" = "以時鐘時間顯示重置時間";
"show_reset_time_as_clock_subtitle" = "將重置時間顯示為絕對時鐘值,而不是倒數計時。";
"show_provider_changelog_links_title" = "顯示提供者版本資訊連結";
Expand Down
5 changes: 3 additions & 2 deletions Sources/CodexBar/UsageStore+HistoricalPace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ extension UsageStore {
func weeklyPace(provider: UsageProvider, window: RateWindow, now: Date = .init()) -> UsagePace? {
guard window.remainingPercent > 0 else { return nil }
let resolved: UsagePace?
let workDays = self.settings.weeklyProgressWorkDays
// Codex can refine pace with historical samples because its dashboard exposes enough weekly history to build
// an account-scoped usage curve. Other providers should not need a hard-coded allowlist: if their RateWindow
// includes a reset time and window duration, the generic linear pace calculation is already defensible.
Expand All @@ -22,14 +23,14 @@ extension UsageStore {
{
resolved = historical
} else {
resolved = UsagePace.weekly(window: window, now: now, defaultWindowMinutes: 10080)
resolved = UsagePace.weekly(window: window, now: now, defaultWindowMinutes: 10080, workDays: workDays)
}
} else {
// Generic providers must carry an explicit window duration. Using the 10080-minute fallback for
// windows without windowMinutes would fabricate a weekly pace for non-weekly windows
// (e.g. Factory monthly with only resetsAt).
guard window.windowMinutes != nil else { return nil }
resolved = UsagePace.weekly(window: window, now: now, defaultWindowMinutes: 10080)
resolved = UsagePace.weekly(window: window, now: now, defaultWindowMinutes: 10080, workDays: workDays)
}

guard let resolved else { return nil }
Expand Down
116 changes: 110 additions & 6 deletions Sources/CodexBarCore/UsagePace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ public struct UsagePace: Sendable {
public static func weekly(
window: RateWindow,
now: Date = .init(),
defaultWindowMinutes: Int = 10080) -> UsagePace?
defaultWindowMinutes: Int = 10080,
workDays: Int? = nil) -> UsagePace?
{
guard let resetsAt = window.resetsAt else { return nil }
let minutes = window.windowMinutes ?? defaultWindowMinutes
Expand All @@ -51,7 +52,15 @@ public struct UsagePace: Sendable {
guard timeUntilReset > 0 else { return nil }
guard timeUntilReset <= duration else { return nil }
let elapsed = (duration - timeUntilReset).clamped(to: 0...duration)
let expected = ((elapsed / duration) * 100).clamped(to: 0...100)
let workdayProgress: WorkdayProgress? = if let workDays, workDays >= 2, workDays < 7,
minutes == 10080
{
Self.workdayProgress(now: now, duration: duration, resetsAt: resetsAt, workDays: workDays)
} else {
nil
}
let expected = workdayProgress?.expectedUsedPercent
?? ((elapsed / duration) * 100).clamped(to: 0...100)
let actual = window.usedPercent.clamped(to: 0...100)
if elapsed == 0, actual > 0 {
return nil
Expand All @@ -62,18 +71,26 @@ public struct UsagePace: Sendable {
var etaSeconds: TimeInterval?
var willLastToReset = false

if elapsed > 0, actual > 0 {
let rate = actual / elapsed
let paceElapsed = workdayProgress?.elapsedSeconds ?? elapsed
if paceElapsed > 0, actual > 0 {
let rate = actual / paceElapsed
if rate > 0 {
let remaining = max(0, 100 - actual)
let candidate = remaining / rate
if candidate >= timeUntilReset {
let effectiveTimeUntilReset = workdayProgress?.remainingSeconds ?? timeUntilReset
if candidate >= effectiveTimeUntilReset {
willLastToReset = true
} else if let workDays = workdayProgress?.workDays {
etaSeconds = Self.wallClockInterval(
from: now,
to: resetsAt,
consumingWorkSeconds: candidate,
workDays: workDays)
} else {
etaSeconds = candidate
}
}
} else if elapsed > 0, actual == 0 {
} else if paceElapsed > 0, actual == 0 {
willLastToReset = true
}

Expand Down Expand Up @@ -107,6 +124,93 @@ public struct UsagePace: Sendable {
runOutProbability: runOutProbability)
}

private struct WorkdayProgress {
let workDays: Int
let totalSeconds: TimeInterval
let elapsedSeconds: TimeInterval
let remainingSeconds: TimeInterval

var expectedUsedPercent: Double {
((self.elapsedSeconds / self.totalSeconds) * 100).clamped(to: 0...100)
}
}

/// Splits the weekly window at local day boundaries so reset offsets do not shift weekday classification.
private static func workdayProgress(
now: Date,
duration: TimeInterval,
resetsAt: Date,
workDays: Int) -> WorkdayProgress?
{
let windowStart = resetsAt.addingTimeInterval(-duration)

var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = .current

var totalWorkSeconds: TimeInterval = 0
var elapsedWorkSeconds: TimeInterval = 0
var remainingWorkSeconds: TimeInterval = 0

var cursor = windowStart
while cursor < resetsAt {
let startOfNextDay = calendar.startOfDay(for: calendar.date(byAdding: .day, value: 1, to: cursor)!)
let sliceEnd = min(startOfNextDay, resetsAt)

if Self.isWorkday(cursor, calendar: calendar, workDays: workDays) {
let sliceDuration = sliceEnd.timeIntervalSince(cursor)
totalWorkSeconds += sliceDuration
if now > cursor {
elapsedWorkSeconds += min(now, sliceEnd).timeIntervalSince(cursor)
}
if now < sliceEnd {
remainingWorkSeconds += sliceEnd.timeIntervalSince(max(now, cursor))
}
}
cursor = sliceEnd
}

guard totalWorkSeconds > 0 else { return nil }
return WorkdayProgress(
workDays: workDays,
totalSeconds: totalWorkSeconds,
elapsedSeconds: elapsedWorkSeconds,
remainingSeconds: remainingWorkSeconds)
}

private static func wallClockInterval(
from now: Date,
to resetsAt: Date,
consumingWorkSeconds requiredWorkSeconds: TimeInterval,
workDays: Int) -> TimeInterval?
{
guard requiredWorkSeconds > 0 else { return 0 }

var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = .current

var remaining = requiredWorkSeconds
var cursor = now
while cursor < resetsAt {
let startOfNextDay = calendar.startOfDay(for: calendar.date(byAdding: .day, value: 1, to: cursor)!)
let sliceEnd = min(startOfNextDay, resetsAt)
if Self.isWorkday(cursor, calendar: calendar, workDays: workDays) {
let available = sliceEnd.timeIntervalSince(cursor)
if remaining <= available {
return cursor.addingTimeInterval(remaining).timeIntervalSince(now)
}
remaining -= available
}
cursor = sliceEnd
}
return nil
}

private static func isWorkday(_ date: Date, calendar: Calendar, workDays: Int) -> Bool {
let weekday = calendar.component(.weekday, from: date)
let isoWeekday = weekday == 1 ? 7 : weekday - 1
return isoWeekday <= workDays
}

private static func stage(for delta: Double) -> Stage {
let absDelta = abs(delta)
if absDelta <= 2 { return .onTrack }
Expand Down
Loading
Loading