From 2c508fce982c6f4f6bbd7acc2cfe254280329960 Mon Sep 17 00:00:00 2001 From: astuteprogrammer Date: Sun, 28 Jun 2026 15:01:44 -0700 Subject: [PATCH 1/6] Add Codex pace headroom hint --- Sources/CodexBar/HistoricalUsagePace.swift | 11 +++- .../CodexBar/PreferencesAdvancedPane.swift | 22 +++++++ .../Resources/ar.lproj/Localizable.strings | 4 ++ .../Resources/ca.lproj/Localizable.strings | 4 ++ .../Resources/de.lproj/Localizable.strings | 1 + .../Resources/en.lproj/Localizable.strings | 4 ++ .../Resources/es.lproj/Localizable.strings | 3 + .../Resources/fa.lproj/Localizable.strings | 4 ++ .../Resources/fr.lproj/Localizable.strings | 4 ++ .../Resources/id.lproj/Localizable.strings | 4 ++ .../Resources/it.lproj/Localizable.strings | 4 ++ .../Resources/ja.lproj/Localizable.strings | 4 ++ .../Resources/ko.lproj/Localizable.strings | 4 ++ .../Resources/nl.lproj/Localizable.strings | 4 ++ .../Resources/pl.lproj/Localizable.strings | 4 ++ .../Resources/pt-BR.lproj/Localizable.strings | 4 ++ .../Resources/sv.lproj/Localizable.strings | 4 ++ .../Resources/th.lproj/Localizable.strings | 4 ++ .../Resources/tr.lproj/Localizable.strings | 4 ++ .../Resources/uk.lproj/Localizable.strings | 4 ++ .../Resources/vi.lproj/Localizable.strings | 4 ++ .../zh-Hans.lproj/Localizable.strings | 4 ++ .../zh-Hant.lproj/Localizable.strings | 3 + Sources/CodexBar/SettingsStore+Defaults.swift | 9 +++ .../SettingsStore+MenuObservation.swift | 1 + Sources/CodexBar/SettingsStore.swift | 3 + Sources/CodexBar/SettingsStoreState.swift | 1 + Sources/CodexBar/UsagePaceText.swift | 17 +++++- .../CodexBar/UsageStore+HistoricalPace.swift | 3 +- Sources/CodexBarCLI/CLIRenderer.swift | 17 +++++- .../Providers/Ollama/OllamaUsageFetcher.swift | 59 ++++++++++++++----- Sources/CodexBarCore/UsagePace.swift | 8 +++ Tests/CodexBarTests/CLISnapshotTests.swift | 5 +- .../SettingsStoreCoverageTests.swift | 7 +++ Tests/CodexBarTests/UsagePaceTests.swift | 1 + Tests/CodexBarTests/UsagePaceTextTests.swift | 23 +++++++- 36 files changed, 239 insertions(+), 27 deletions(-) diff --git a/Sources/CodexBar/HistoricalUsagePace.swift b/Sources/CodexBar/HistoricalUsagePace.swift index f43ebceab8..4e75d4da67 100644 --- a/Sources/CodexBar/HistoricalUsagePace.swift +++ b/Sources/CodexBar/HistoricalUsagePace.swift @@ -758,13 +758,18 @@ actor HistoricalUsageHistoryStore { } enum CodexHistoricalPaceEvaluator { - static let minimumCompleteWeeksForHistorical = 3 + static let defaultMinimumCompleteWeeksForHistorical = 3 static let minimumWeeksForRisk = 5 private static let recencyTauWeeks: Double = 3 private static let epsilon: Double = 1e-9 private static let resetBucketSeconds: TimeInterval = 5 * 60 - static func evaluate(window: RateWindow, now: Date, dataset: CodexHistoricalDataset?) -> UsagePace? { + static func evaluate( + window: RateWindow, + now: Date, + dataset: CodexHistoricalDataset?, + minimumCompleteWeeks: Int = Self.defaultMinimumCompleteWeeksForHistorical) -> UsagePace? + { guard let dataset else { return nil } guard let resetsAt = window.resetsAt else { return nil } let minutes = window.windowMinutes ?? 10080 @@ -785,7 +790,7 @@ enum CodexHistoricalPaceEvaluator { let scopedWeeks = dataset.weeks.filter { week in week.windowMinutes == minutes && week.resetsAt < normalizedResetsAt } - guard scopedWeeks.count >= Self.minimumCompleteWeeksForHistorical else { return nil } + guard scopedWeeks.count >= max(2, minimumCompleteWeeks) else { return nil } let weightedWeeks = scopedWeeks.map { week in let ageWeeks = Self.clamp( diff --git a/Sources/CodexBar/PreferencesAdvancedPane.swift b/Sources/CodexBar/PreferencesAdvancedPane.swift index 1545eba847..f686165e5e 100644 --- a/Sources/CodexBar/PreferencesAdvancedPane.swift +++ b/Sources/CodexBar/PreferencesAdvancedPane.swift @@ -72,6 +72,28 @@ struct AdvancedPane: View { Divider() + SettingsSection(contentSpacing: 10) { + HStack(alignment: .center, spacing: 12) { + VStack(alignment: .leading, spacing: 4) { + Text(L("historical_pace_weeks_title")) + .font(.body) + Text(L("historical_pace_weeks_subtitle")) + .font(.footnote) + .foregroundStyle(.tertiary) + } + Spacer(minLength: 12) + Stepper(value: self.$settings.historicalPaceMinimumWeeks, in: 2...8, step: 1) { + Text(String(format: L("historical_pace_weeks_value"), self.settings.historicalPaceMinimumWeeks)) + .font(.footnote) + .foregroundStyle(.secondary) + .monospacedDigit() + .frame(minWidth: 58, alignment: .trailing) + } + } + } + + Divider() + SettingsSection(contentSpacing: 10) { PreferenceToggleRow( title: L("hide_personal_info_title"), diff --git a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings index 8fa918882e..08c2b64f0b 100644 --- a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings @@ -547,6 +547,9 @@ "no_writable_bin_dirs" = "لم يتم العثور على أي سجلات قابلة للكتابة."; "show_debug_settings_title" = "عرض إعدادات التصحيح"; "show_debug_settings_subtitle" = "اعرض أدوات استكشاف الأخطاء في تبويب التصحيح."; +"historical_pace_weeks_title" = "Historical pace weeks"; +"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; +"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "فاجئني"; "surprise_me_subtitle" = "تحقق إذا كنت تحب وكلائنك يستمتعون هناك."; "weekly_limit_confetti_title" = "قصاصات كونفيتي أسبوعية محدودة"; @@ -724,6 +727,7 @@ "Resets now" = "إعادة التعيين الآن"; "reset_tomorrow_format" = "غدًا، %@"; "Lasts until reset" = "يستمر حتى إعادة التعيين"; +"Try 1.5x!" = "Try 1.5x!"; "Updated %@" = "تحديث %@"; "Updated %@h ago" = "تم التحديث %@h قبل"; "Updated %@m ago" = "تم التحديث %@m قبل"; diff --git a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings index 0b4b343a8c..c735065e11 100644 --- a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings @@ -536,6 +536,9 @@ "no_writable_bin_dirs" = "No s'han trobat directoris bin amb permís d'escriptura."; "show_debug_settings_title" = "Mostreu la configuració de depuració"; "show_debug_settings_subtitle" = "Mostreu eines de diagnòstic a la pestanya Depuració."; +"historical_pace_weeks_title" = "Historical pace weeks"; +"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; +"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Sorprèn-me"; "surprise_me_subtitle" = "Activeu-ho si us agrada que els vostres agents es diverteixin allà dalt."; "weekly_limit_confetti_title" = "Confeti del límit setmanal"; @@ -1028,6 +1031,7 @@ "Last 30 days: %@" = "Últims 30 dies: %@"; "Last 30 days: %@ · %@ tokens" = "Últims 30 dies: %@ · %@ tokens"; "Lasts until reset" = "Dura fins al reinici"; +"Try 1.5x!" = "Try 1.5x!"; "Login with Google" = "Inicieu sessió amb Google"; "login_success_notification_body" = "Podeu tornar a l'app; l'autenticació ha finalitzat."; "login_success_notification_title" = "Inici de sessió de %@ correcte"; diff --git a/Sources/CodexBar/Resources/de.lproj/Localizable.strings b/Sources/CodexBar/Resources/de.lproj/Localizable.strings index de6a643944..45681a740d 100644 --- a/Sources/CodexBar/Resources/de.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/de.lproj/Localizable.strings @@ -724,6 +724,7 @@ "Resets now" = "Wird jetzt zurückgesetzt"; "reset_tomorrow_format" = "morgen, %@"; "Lasts until reset" = "Hält bis zum Zurücksetzen an"; +"Try 1.5x!" = "Try 1.5x!"; "Updated %@" = "Aktualisiert %@"; "Updated %@h ago" = "Vor %@h aktualisiert"; "Updated %@m ago" = "Vor %@m aktualisiert"; diff --git a/Sources/CodexBar/Resources/en.lproj/Localizable.strings b/Sources/CodexBar/Resources/en.lproj/Localizable.strings index f44ff85561..39ffbbdb48 100644 --- a/Sources/CodexBar/Resources/en.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/en.lproj/Localizable.strings @@ -547,6 +547,9 @@ "no_writable_bin_dirs" = "No writable bin dirs found."; "show_debug_settings_title" = "Show Debug Settings"; "show_debug_settings_subtitle" = "Expose troubleshooting tools in the Debug tab."; +"historical_pace_weeks_title" = "Historical pace weeks"; +"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; +"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Surprise me"; "surprise_me_subtitle" = "Check if you like your agents having some fun up there."; "weekly_limit_confetti_title" = "Weekly limit confetti"; @@ -724,6 +727,7 @@ "Resets now" = "Resets now"; "reset_tomorrow_format" = "tomorrow, %@"; "Lasts until reset" = "Lasts until reset"; +"Try 1.5x!" = "Try 1.5x!"; "Updated %@" = "Updated %@"; "Updated %@h ago" = "Updated %@h ago"; "Updated %@m ago" = "Updated %@m ago"; diff --git a/Sources/CodexBar/Resources/es.lproj/Localizable.strings b/Sources/CodexBar/Resources/es.lproj/Localizable.strings index 490ef41397..ca3f73b8fc 100644 --- a/Sources/CodexBar/Resources/es.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/es.lproj/Localizable.strings @@ -536,6 +536,9 @@ "no_writable_bin_dirs" = "No se encontraron directorios bin con permiso de escritura."; "show_debug_settings_title" = "Mostrar ajustes de depuración"; "show_debug_settings_subtitle" = "Muestra herramientas de diagnóstico en la pestaña Depuración."; +"historical_pace_weeks_title" = "Historical pace weeks"; +"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; +"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Sorpréndeme"; "surprise_me_subtitle" = "Actívalo si te gusta que tus agentes se diviertan ahí arriba."; "weekly_limit_confetti_title" = "Confeti del límite semanal"; diff --git a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings index be28b18eb5..1d9919afdd 100644 --- a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings @@ -547,6 +547,9 @@ "no_writable_bin_dirs" = "هیچ سطل قابل نوشتاری پیدا نشد."; "show_debug_settings_title" = "نمایش تنظیمات اشکال زدایی"; "show_debug_settings_subtitle" = "ابزارهای عیب یابی را در تب اشکال زدایی آشکار کنید."; +"historical_pace_weeks_title" = "Historical pace weeks"; +"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; +"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "سورپرایزم کن"; "surprise_me_subtitle" = "بررسی کن که آیا دوست داری مأمورانت آنجا خوش بگذرانند یا نه."; "weekly_limit_confetti_title" = "کنفتی محدود هفتگی"; @@ -724,6 +727,7 @@ "Resets now" = "اکنون بازنشانی می شود"; "reset_tomorrow_format" = "فردا، %@"; "Lasts until reset" = "تا زمان ریست ادامه دارد"; +"Try 1.5x!" = "Try 1.5x!"; "Updated %@" = "به روزرسانی %@"; "Updated %@h ago" = "%@h پیش به روزرسانی شده است"; "Updated %@m ago" = "%@m پیش به روزرسانی شده"; diff --git a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings index c9dad3f12c..6720cce3c6 100644 --- a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings @@ -547,6 +547,9 @@ "no_writable_bin_dirs" = "Aucun répertoire bin inscriptible trouvé."; "show_debug_settings_title" = "Afficher les paramètres de débogage"; "show_debug_settings_subtitle" = "Exposez les outils de dépannage dans l’onglet Débogage."; +"historical_pace_weeks_title" = "Historical pace weeks"; +"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; +"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Surprenez-moi"; "surprise_me_subtitle" = "Vérifiez si vous aimez que vos agents s'amusent là-haut."; "weekly_limit_confetti_title" = "Confettis de limite hebdomadaire"; @@ -724,6 +727,7 @@ "Resets now" = "Réinitialise maintenant"; "reset_tomorrow_format" = "demain, %@"; "Lasts until reset" = "Dure jusqu'à la réinitialisation"; +"Try 1.5x!" = "Try 1.5x!"; "Updated %@" = "%@ mis à jour"; "Updated %@h ago" = "Mis à jour il y a %@h"; "Updated %@m ago" = "Mis à jour il y a %@m"; diff --git a/Sources/CodexBar/Resources/id.lproj/Localizable.strings b/Sources/CodexBar/Resources/id.lproj/Localizable.strings index c158d02154..76bf2672fe 100644 --- a/Sources/CodexBar/Resources/id.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/id.lproj/Localizable.strings @@ -549,6 +549,9 @@ "no_writable_bin_dirs" = "Tidak ada direktori bin yang dapat ditulis."; "show_debug_settings_title" = "Tampilkan Pengaturan Debug"; "show_debug_settings_subtitle" = "Tampilkan alat pemecahan masalah di tab Debug."; +"historical_pace_weeks_title" = "Historical pace weeks"; +"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; +"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Kejutkan saya"; "surprise_me_subtitle" = "Centang jika Anda suka agen bersenang-senang di atas sana."; "weekly_limit_confetti_title" = "Confetti batas mingguan"; @@ -726,6 +729,7 @@ "Resets now" = "Reset sekarang"; "reset_tomorrow_format" = "besok, %@"; "Lasts until reset" = "Bertahan hingga reset"; +"Try 1.5x!" = "Try 1.5x!"; "Updated %@" = "Diperbarui %@"; "Updated %@h ago" = "Diperbarui %@ jam lalu"; "Updated %@m ago" = "Diperbarui %@ menit lalu"; diff --git a/Sources/CodexBar/Resources/it.lproj/Localizable.strings b/Sources/CodexBar/Resources/it.lproj/Localizable.strings index f4603b98fe..72287af2a6 100644 --- a/Sources/CodexBar/Resources/it.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/it.lproj/Localizable.strings @@ -549,6 +549,9 @@ "no_writable_bin_dirs" = "Nessuna directory bin scrivibile trovata."; "show_debug_settings_title" = "Mostra impostazioni debug"; "show_debug_settings_subtitle" = "Espone strumenti di diagnosi nella scheda Debug."; +"historical_pace_weeks_title" = "Historical pace weeks"; +"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; +"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Sorprendimi"; "surprise_me_subtitle" = "Per chi apprezza un po' di personalità dagli agenti."; "weekly_limit_confetti_title" = "Coriandoli limite settimanale"; @@ -726,6 +729,7 @@ "Resets now" = "Si resetta ora"; "reset_tomorrow_format" = "domani, %@"; "Lasts until reset" = "Valido fino al reset"; +"Try 1.5x!" = "Try 1.5x!"; "Updated %@" = "Aggiornato %@"; "Updated %@h ago" = "Aggiornato %@ h fa"; "Updated %@m ago" = "Aggiornato %@ min fa"; diff --git a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings index bca83f55fb..c68332f29f 100644 --- a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings @@ -544,6 +544,9 @@ "no_writable_bin_dirs" = "書き込み可能な bin ディレクトリが見つかりません。"; "show_debug_settings_title" = "デバッグ設定を表示"; "show_debug_settings_subtitle" = "デバッグタブにトラブルシューティングツールを表示します。"; +"historical_pace_weeks_title" = "Historical pace weeks"; +"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; +"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "サプライズ"; "surprise_me_subtitle" = "エージェントたちがメニューバーで少し遊ぶのが好きか試してみてください。"; "weekly_limit_confetti_title" = "週間上限の紙吹雪"; @@ -721,6 +724,7 @@ "Resets now" = "まもなくリセット"; "reset_tomorrow_format" = "明日 %@"; "Lasts until reset" = "リセットまで持続"; +"Try 1.5x!" = "Try 1.5x!"; "Updated %@" = "%@ に更新"; "Updated %@h ago" = "%@時間前に更新"; "Updated %@m ago" = "%@分前に更新"; diff --git a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings index ad20abb7dd..4c4af6beb4 100644 --- a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings @@ -536,6 +536,9 @@ "no_writable_bin_dirs" = "쓰기 가능한 bin 디렉터리를 찾을 수 없습니다."; "show_debug_settings_title" = "디버그 설정 표시"; "show_debug_settings_subtitle" = "디버그 탭에 문제 해결 도구를 표시합니다."; +"historical_pace_weeks_title" = "Historical pace weeks"; +"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; +"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "깜짝 놀래주기"; "surprise_me_subtitle" = "에이전트가 저 위에서 조금 즐기는 모습이 마음에 드는지 확인해 보세요."; "weekly_limit_confetti_title" = "주간 한도 색종이"; @@ -697,6 +700,7 @@ "Resets now" = "지금 재설정"; "reset_tomorrow_format" = "내일 %@"; "Lasts until reset" = "재설정까지 유지"; +"Try 1.5x!" = "Try 1.5x!"; "Updated %@" = "%@에 업데이트됨"; "Updated %@h ago" = "%@시간 전 업데이트됨"; "Updated %@m ago" = "%@분 전 업데이트됨"; diff --git a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings index 12ef49766d..d34e1e307a 100644 --- a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings @@ -547,6 +547,9 @@ "no_writable_bin_dirs" = "Geen beschrijfbare mapmap gevonden."; "show_debug_settings_title" = "Toon foutopsporingsinstellingen"; "show_debug_settings_subtitle" = "Geef hulpprogramma's voor probleemoplossing weer op het tabblad Foutopsporing."; +"historical_pace_weeks_title" = "Historical pace weeks"; +"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; +"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Verras mij"; "surprise_me_subtitle" = "Controleer of je het leuk vindt dat je agenten daar plezier hebben."; "weekly_limit_confetti_title" = "Wekelijkse limiet confetti"; @@ -724,6 +727,7 @@ "Resets now" = "Wordt nu gereset"; "reset_tomorrow_format" = "morgen, %@"; "Lasts until reset" = "Gaat mee tot reset"; +"Try 1.5x!" = "Try 1.5x!"; "Updated %@" = "Bijgewerkt %@"; "Updated %@h ago" = "%@u geleden bijgewerkt"; "Updated %@m ago" = "%@m geleden bijgewerkt"; diff --git a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings index 0f5f0f6965..0911e6b1be 100644 --- a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings @@ -549,6 +549,9 @@ "no_writable_bin_dirs" = "Nie znaleziono zapisywalnych katalogów bin."; "show_debug_settings_title" = "Pokaż ustawienia debugowania"; "show_debug_settings_subtitle" = "Pokazuje dodatkowe ustawienia debugowania i diagnostyki."; +"historical_pace_weeks_title" = "Historical pace weeks"; +"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; +"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Zaskocz mnie"; "surprise_me_subtitle" = "Sprawdź, czy lubisz, gdy twoi agenci trochę się tam bawią."; "weekly_limit_confetti_title" = "Konfetti limitu tygodniowego"; @@ -726,6 +729,7 @@ "Resets now" = "Reset teraz"; "reset_tomorrow_format" = "jutro, %@"; "Lasts until reset" = "Wystarcza do resetu"; +"Try 1.5x!" = "Try 1.5x!"; "Updated %@" = "Zaktualizowano %@"; "Updated %@h ago" = "Zaktualizowano %@ godz. temu"; "Updated %@m ago" = "Zaktualizowano %@ min temu"; diff --git a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings index ed3f0f947e..78a383b567 100644 --- a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings @@ -544,6 +544,9 @@ "no_writable_bin_dirs" = "Nenhum diretório bin gravável encontrado."; "show_debug_settings_title" = "Mostrar ajustes de depuração"; "show_debug_settings_subtitle" = "Exibe ferramentas de diagnóstico na aba Depuração."; +"historical_pace_weeks_title" = "Historical pace weeks"; +"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; +"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Surpreenda-me"; "surprise_me_subtitle" = "Veja se você gosta dos seus agentes se divertindo ali em cima."; "weekly_limit_confetti_title" = "Confete do limite semanal"; @@ -721,6 +724,7 @@ "Resets now" = "Renova agora"; "reset_tomorrow_format" = "amanhã, %@"; "Lasts until reset" = "Dura até a renovação"; +"Try 1.5x!" = "Try 1.5x!"; "Updated %@" = "Atualizado %@"; "Updated %@h ago" = "Atualizado há %@h"; "Updated %@m ago" = "Atualizado há %@m"; diff --git a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings index f4a2dd49e8..a0d659b7f8 100644 --- a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings @@ -546,6 +546,9 @@ "no_writable_bin_dirs" = "Inga skrivbara bin-kataloger hittades."; "show_debug_settings_title" = "Visa felsökningsinställningar"; "show_debug_settings_subtitle" = "Visa felsökningsverktyg på fliken Felsök."; +"historical_pace_weeks_title" = "Historical pace weeks"; +"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; +"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Överraska mig"; "surprise_me_subtitle" = "Kontrollera om du vill att agenterna ska få leka lite där uppe."; "weekly_limit_confetti_title" = "Veckogränskonfetti"; @@ -723,6 +726,7 @@ "Resets now" = "Återställs nu"; "reset_tomorrow_format" = "imorgon %@"; "Lasts until reset" = "Räcker till återställning"; +"Try 1.5x!" = "Try 1.5x!"; "Updated %@" = "Uppdaterad %@"; "Updated %@h ago" = "Uppdaterad för %@ h sedan"; "Updated %@m ago" = "Uppdaterad för %@ min sedan"; diff --git a/Sources/CodexBar/Resources/th.lproj/Localizable.strings b/Sources/CodexBar/Resources/th.lproj/Localizable.strings index 6f300f493f..926852f43c 100644 --- a/Sources/CodexBar/Resources/th.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/th.lproj/Localizable.strings @@ -547,6 +547,9 @@ "no_writable_bin_dirs" = "ไม่พบ bin dirs ที่เขียนได้"; "show_debug_settings_title" = "แสดงการตั้งค่าการดีบัก"; "show_debug_settings_subtitle" = "แสดงเครื่องมือการแก้ไขปัญหาในแท็บ แก้ไขข้อบกพร่อง"; +"historical_pace_weeks_title" = "Historical pace weeks"; +"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; +"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "ทําให้ฉันประหลาดใจ"; "surprise_me_subtitle" = "ตรวจสอบว่าคุณชอบให้ตัวแทนของคุณสนุกสนานที่นั่นหรือไม่"; "weekly_limit_confetti_title" = "ลูกปาจํากัดรายสัปดาห์"; @@ -724,6 +727,7 @@ "Resets now" = "รีเซ็ตเดี๋ยวนี้"; "reset_tomorrow_format" = "พรุ่งนี้ %@"; "Lasts until reset" = "คงอยู่จนกว่าจะรีเซ็ต"; +"Try 1.5x!" = "Try 1.5x!"; "Updated %@" = "อัพเดท %@"; "Updated %@h ago" = "อัพเดท %@h ที่ผ่านมา"; "Updated %@m ago" = "อัพเดท %@m ที่ผ่านมา"; diff --git a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings index 537bdc99e8..b3b6bb23de 100644 --- a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings @@ -547,6 +547,9 @@ "no_writable_bin_dirs" = "Yazılabilir bin dizini bulunamadı."; "show_debug_settings_title" = "Hata Ayıklama Ayarlarını Göster"; "show_debug_settings_subtitle" = "Sorun giderme araçlarını Hata Ayıklama sekmesinde göster."; +"historical_pace_weeks_title" = "Historical pace weeks"; +"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; +"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Beni şaşırt"; "surprise_me_subtitle" = "Ajanlarınızın yukarıda biraz eğlenmesini istiyorsanız işaretleyin."; "weekly_limit_confetti_title" = "Haftalık limit konfetisi"; @@ -722,6 +725,7 @@ "Resets now" = "Şimdi sıfırlanır"; "reset_tomorrow_format" = "yarın, %@"; "Lasts until reset" = "Sıfırlamaya kadar sürer"; +"Try 1.5x!" = "Try 1.5x!"; "Updated %@" = "Güncellendi: %@"; "Updated %@h ago" = "%@ saat önce güncellendi"; "Updated %@m ago" = "%@ dakika önce güncellendi"; diff --git a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings index 4c074db5eb..9c1b2b6992 100644 --- a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings @@ -547,6 +547,9 @@ "no_writable_bin_dirs" = "Не знайдено записуваних каталогів кошика."; "show_debug_settings_title" = "Показати налаштування налагодження"; "show_debug_settings_subtitle" = "Розкрийте інструменти усунення несправностей на вкладці Debug."; +"historical_pace_weeks_title" = "Historical pace weeks"; +"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; +"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Здивуйте мене"; "surprise_me_subtitle" = "Перевірте, чи подобається вам, що ваші агенти розважаються там."; "weekly_limit_confetti_title" = "Щотижневий ліміт конфетті"; @@ -724,6 +727,7 @@ "Resets now" = "Скидає зараз"; "reset_tomorrow_format" = "завтра, %@"; "Lasts until reset" = "Триває до скидання"; +"Try 1.5x!" = "Try 1.5x!"; "Updated %@" = "Оновлено %@"; "Updated %@h ago" = "Оновлено %@ год тому"; "Updated %@m ago" = "Оновлено %@хв тому"; diff --git a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings index 8aa0077a5c..9e34aa27e8 100644 --- a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings @@ -543,6 +543,9 @@ "no_writable_bin_dirs" = "Không tìm thấy thư mục bin có thể ghi."; "show_debug_settings_title" = "Hiển thị gỡ lỗi Cài đặt"; "show_debug_settings_subtitle" = "Hiển thị các công cụ khắc phục sự cố trong tab Gỡ lỗi."; +"historical_pace_weeks_title" = "Historical pace weeks"; +"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; +"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Làm tôi ngạc nhiên"; "surprise_me_subtitle" = "Kiểm tra xem bạn có thích các đại lý của mình vui vẻ ở đó không."; "weekly_limit_confetti_title" = "Hoa giấy giới hạn hàng tuần"; @@ -720,6 +723,7 @@ "Resets now" = "Đặt lại ngay"; "reset_tomorrow_format" = "ngày mai, %@"; "Lasts until reset" = "Kéo dài cho đến Đặt lại"; +"Try 1.5x!" = "Try 1.5x!"; "Updated %@" = "Đã cập nhật %@"; "Updated %@h ago" = "Đã cập nhật %@ h trước"; "Updated %@m ago" = "Đã cập nhật %@ tháng trước"; diff --git a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings index 8553ce59c2..2b7347cdd6 100644 --- a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings @@ -532,6 +532,9 @@ "no_writable_bin_dirs" = "未找到可写的 bin 目录。"; "show_debug_settings_title" = "显示调试设置"; "show_debug_settings_subtitle" = "在“调试”标签中显示故障排除工具。"; +"historical_pace_weeks_title" = "Historical pace weeks"; +"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; +"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "给我惊喜"; "surprise_me_subtitle" = "看看你是否喜欢你的智能体在上面找点乐子。"; "weekly_limit_confetti_title" = "每周限制彩纸"; @@ -698,6 +701,7 @@ "Resets now" = "立即重置"; "reset_tomorrow_format" = "明天 %@"; "Lasts until reset" = "持续到重置"; +"Try 1.5x!" = "Try 1.5x!"; "Updated %@" = "更新于 %@"; "Updated %@h ago" = "%@ 小时前更新"; "Updated %@m ago" = "%@ 分钟前更新"; diff --git a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings index 08f1efb0f7..9532e06cda 100644 --- a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings @@ -543,6 +543,9 @@ "no_writable_bin_dirs" = "找不到可寫的 bin 目錄。"; "show_debug_settings_title" = "顯示除錯設定"; "show_debug_settings_subtitle" = "在「除錯」標籤中顯示疑難排解工具。"; +"historical_pace_weeks_title" = "Historical pace weeks"; +"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; +"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "給我驚喜"; "surprise_me_subtitle" = "讓選單列上的 Agent 多一點變化。"; "weekly_limit_confetti_title" = "每週重置慶祝動畫"; diff --git a/Sources/CodexBar/SettingsStore+Defaults.swift b/Sources/CodexBar/SettingsStore+Defaults.swift index 16f35a1ff4..bbc28d040c 100644 --- a/Sources/CodexBar/SettingsStore+Defaults.swift +++ b/Sources/CodexBar/SettingsStore+Defaults.swift @@ -288,6 +288,15 @@ extension SettingsStore { } } + var historicalPaceMinimumWeeks: Int { + get { self.defaultsState.historicalPaceMinimumWeeks } + set { + let clamped = max(2, min(8, newValue)) + self.defaultsState.historicalPaceMinimumWeeks = clamped + self.userDefaults.set(clamped, forKey: "historicalPaceMinimumWeeks") + } + } + var menuBarMetricPreferencesRaw: [String: String] { get { self.defaultsState.menuBarMetricPreferencesRaw } set { diff --git a/Sources/CodexBar/SettingsStore+MenuObservation.swift b/Sources/CodexBar/SettingsStore+MenuObservation.swift index ce0ea85e3c..c13c76f8c8 100644 --- a/Sources/CodexBar/SettingsStore+MenuObservation.swift +++ b/Sources/CodexBar/SettingsStore+MenuObservation.swift @@ -29,6 +29,7 @@ extension SettingsStore { _ = self.menuBarDisplayMode _ = self.kiroMenuBarDisplayMode _ = self.historicalTrackingEnabled + _ = self.historicalPaceMinimumWeeks _ = self.multiAccountMenuLayout _ = self.menuBarMetricPreferencesRaw _ = self.copilotIconSecondaryWindowIDRaw diff --git a/Sources/CodexBar/SettingsStore.swift b/Sources/CodexBar/SettingsStore.swift index 33d35e3e47..cfd8ad1416 100644 --- a/Sources/CodexBar/SettingsStore.swift +++ b/Sources/CodexBar/SettingsStore.swift @@ -404,6 +404,8 @@ extension SettingsStore { let kiroMenuBarDisplayModeRaw = userDefaults.string(forKey: "kiroMenuBarDisplayMode") ?? KiroMenuBarDisplayMode.automatic.rawValue let historicalTrackingEnabled = userDefaults.object(forKey: "historicalTrackingEnabled") as? Bool ?? false + let rawHistoricalPaceMinimumWeeks = userDefaults.object(forKey: "historicalPaceMinimumWeeks") as? Int ?? 3 + let historicalPaceMinimumWeeks = max(2, min(8, rawHistoricalPaceMinimumWeeks)) let multiAccountMenuLayoutRaw = Self.loadMultiAccountMenuLayoutRaw(userDefaults: userDefaults) let resolvedPreferences = Self.loadMenuBarMetricPreferences(userDefaults: userDefaults) let copilotBudgetExtrasEnabled = userDefaults.object(forKey: "copilotBudgetExtrasEnabled") as? Bool ?? false @@ -482,6 +484,7 @@ extension SettingsStore { menuBarDisplayModeRaw: menuBarDisplayModeRaw, kiroMenuBarDisplayModeRaw: kiroMenuBarDisplayModeRaw, historicalTrackingEnabled: historicalTrackingEnabled, + historicalPaceMinimumWeeks: historicalPaceMinimumWeeks, multiAccountMenuLayoutRaw: multiAccountMenuLayoutRaw, menuBarMetricPreferencesRaw: resolvedPreferences, copilotBudgetExtrasEnabled: copilotBudgetExtrasEnabled, diff --git a/Sources/CodexBar/SettingsStoreState.swift b/Sources/CodexBar/SettingsStoreState.swift index 23484b9241..7e0c1d2901 100644 --- a/Sources/CodexBar/SettingsStoreState.swift +++ b/Sources/CodexBar/SettingsStoreState.swift @@ -28,6 +28,7 @@ struct SettingsDefaultsState { var menuBarDisplayModeRaw: String? var kiroMenuBarDisplayModeRaw: String? var historicalTrackingEnabled: Bool + var historicalPaceMinimumWeeks: Int var multiAccountMenuLayoutRaw: String var menuBarMetricPreferencesRaw: [String: String] var copilotBudgetExtrasEnabled: Bool diff --git a/Sources/CodexBar/UsagePaceText.swift b/Sources/CodexBar/UsagePaceText.swift index 5a57483109..79a6be1815 100644 --- a/Sources/CodexBar/UsagePaceText.swift +++ b/Sources/CodexBar/UsagePaceText.swift @@ -45,7 +45,7 @@ enum UsagePaceText { private static func detailRightLabel(for pace: UsagePace, context: DetailContext, now: Date) -> String? { let etaLabel: String? if pace.willLastToReset { - etaLabel = L("Lasts until reset") + etaLabel = self.combinedLastsLabel(for: pace) } else if let etaSeconds = pace.etaSeconds { let etaText = Self.durationText(seconds: etaSeconds, now: now) if context == .session { @@ -69,6 +69,21 @@ enum UsagePaceText { return riskLabel } + private static func combinedLastsLabel(for pace: UsagePace) -> String { + guard let speedLabel = self.speedHintLabel(for: pace) else { + return L("Lasts until reset") + } + return L("%@ · %@", L("Lasts until reset"), speedLabel) + } + + private static func speedHintLabel(for pace: UsagePace) -> String? { + guard pace.deltaPercent < -15, + let multiplier = pace.speedMultiplierToReset, + multiplier >= 1.5 + else { return nil } + return L("Try 1.5x!") + } + private static func durationText(seconds: TimeInterval, now: Date) -> String { let date = now.addingTimeInterval(seconds) let countdown = UsageFormatter.resetCountdownDescription(from: date, now: now) diff --git a/Sources/CodexBar/UsageStore+HistoricalPace.swift b/Sources/CodexBar/UsageStore+HistoricalPace.swift index cda21506b2..ec9d7a3fda 100644 --- a/Sources/CodexBar/UsageStore+HistoricalPace.swift +++ b/Sources/CodexBar/UsageStore+HistoricalPace.swift @@ -19,7 +19,8 @@ extension UsageStore { let historical = CodexHistoricalPaceEvaluator.evaluate( window: window, now: now, - dataset: self.codexHistoricalDataset) + dataset: self.codexHistoricalDataset, + minimumCompleteWeeks: self.settings.historicalPaceMinimumWeeks) { resolved = historical } else { diff --git a/Sources/CodexBarCLI/CLIRenderer.swift b/Sources/CodexBarCLI/CLIRenderer.swift index 14bfa59501..0e4aa90bd5 100644 --- a/Sources/CodexBarCLI/CLIRenderer.swift +++ b/Sources/CodexBarCLI/CLIRenderer.swift @@ -572,7 +572,7 @@ enum CLIRenderer { } private static func paceRightLabel(for pace: UsagePace, kind: PaceKind, now: Date) -> String? { - if pace.willLastToReset { return "Lasts until reset" } + if pace.willLastToReset { return Self.combinedLastsLabel(for: pace) } guard let etaSeconds = pace.etaSeconds else { return nil } let etaText = Self.paceDurationText(seconds: etaSeconds, now: now) switch kind { @@ -583,6 +583,21 @@ enum CLIRenderer { } } + private static func combinedLastsLabel(for pace: UsagePace) -> String { + guard let speedLabel = Self.speedHintLabel(for: pace) else { + return "Lasts until reset" + } + return "Lasts until reset | \(speedLabel)" + } + + private static func speedHintLabel(for pace: UsagePace) -> String? { + guard pace.deltaPercent < -15, + let multiplier = pace.speedMultiplierToReset, + multiplier >= 1.5 + else { return nil } + return "Try 1.5x!" + } + private static func paceDurationText(seconds: TimeInterval, now: Date) -> String { let date = now.addingTimeInterval(seconds) let countdown = UsageFormatter.resetCountdownDescription(from: date, now: now) diff --git a/Sources/CodexBarCore/Providers/Ollama/OllamaUsageFetcher.swift b/Sources/CodexBarCore/Providers/Ollama/OllamaUsageFetcher.swift index d3aaea9763..984a234a6f 100644 --- a/Sources/CodexBarCore/Providers/Ollama/OllamaUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Ollama/OllamaUsageFetcher.swift @@ -37,6 +37,7 @@ public enum OllamaUsageError: LocalizedError, Sendable { case parseFailed(String) case networkError(String) case noSessionCookie + case browserAccessDenied(details: String) public var errorDescription: String? { switch self { @@ -54,6 +55,8 @@ public enum OllamaUsageError: LocalizedError, Sendable { "Ollama request failed: \(message)" case .noSessionCookie: "No Ollama session cookie found. Please log in to ollama.com in your browser." + case let .browserAccessDenied(details): + "Ollama browser cookies are not readable. \(details)" } } } @@ -82,6 +85,11 @@ public enum OllamaCookieImporter { } } + private struct ImportScanResult { + var sessions: [SessionInfo] + var accessDeniedHints: [String] + } + public static func importSessions( browserDetection: BrowserDetection, preferredBrowsers: [Browser] = [.chrome], @@ -89,23 +97,31 @@ public enum OllamaCookieImporter { logger: ((String) -> Void)? = nil) throws -> [SessionInfo] { let log: (String) -> Void = { msg in logger?("[ollama-cookie] \(msg)") } + var accessDeniedHints: [String] = [] let preferredSources = preferredBrowsers.isEmpty ? ollamaCookieImportOrder.cookieImportCandidates(using: browserDetection) : preferredBrowsers.cookieImportCandidates(using: browserDetection) - let preferredCandidates = self.collectSessionInfo(from: preferredSources, logger: log) - return try self.selectSessionInfosWithFallback( - preferredCandidates: preferredCandidates, - allowFallbackBrowsers: allowFallbackBrowsers, - loadFallbackCandidates: { - guard !preferredBrowsers.isEmpty else { return [] } - let fallbackSources = self.fallbackBrowserSources( - browserDetection: browserDetection, - excluding: preferredSources) - guard !fallbackSources.isEmpty else { return [] } - log("No recognized Ollama session in preferred browsers; trying fallback import order") - return self.collectSessionInfo(from: fallbackSources, logger: log) - }, - logger: log) + let preferredResult = self.collectSessionInfo(from: preferredSources, logger: log) + accessDeniedHints.append(contentsOf: preferredResult.accessDeniedHints) + do { + return try self.selectSessionInfosWithFallback( + preferredCandidates: preferredResult.sessions, + allowFallbackBrowsers: allowFallbackBrowsers, + loadFallbackCandidates: { + guard !preferredBrowsers.isEmpty else { return [] } + let fallbackSources = self.fallbackBrowserSources( + browserDetection: browserDetection, + excluding: preferredSources) + guard !fallbackSources.isEmpty else { return [] } + log("No recognized Ollama session in preferred browsers; trying fallback import order") + let fallbackResult = self.collectSessionInfo(from: fallbackSources, logger: log) + accessDeniedHints.append(contentsOf: fallbackResult.accessDeniedHints) + return fallbackResult.sessions + }, + logger: log) + } catch OllamaUsageError.noSessionCookie where !accessDeniedHints.isEmpty { + throw OllamaUsageError.browserAccessDenied(details: Self.accessDeniedDetails(accessDeniedHints)) + } } public static func importSession( @@ -201,9 +217,10 @@ public enum OllamaCookieImporter { private static func collectSessionInfo( from browserSources: [Browser], - logger: @escaping (String) -> Void) -> [SessionInfo] + logger: @escaping (String) -> Void) -> ImportScanResult { var candidates: [SessionInfo] = [] + var accessDeniedHints: [String] = [] for browserSource in browserSources { do { let query = BrowserCookieQuery(domains: self.cookieDomains) @@ -216,12 +233,18 @@ public enum OllamaCookieImporter { guard !cookies.isEmpty else { continue } candidates.append(SessionInfo(cookies: cookies, sourceLabel: source.label)) } + } catch let error as BrowserCookieError { + BrowserCookieAccessGate.recordIfNeeded(error) + if let hint = error.accessDeniedHint { + accessDeniedHints.append(hint) + } + logger("\(browserSource.displayName) cookie import failed: \(error.localizedDescription)") } catch { BrowserCookieAccessGate.recordIfNeeded(error) logger("\(browserSource.displayName) cookie import failed: \(error.localizedDescription)") } } - return candidates + return ImportScanResult(sessions: candidates, accessDeniedHints: accessDeniedHints) } private static func containsRecognizedSessionCookie(in cookies: [HTTPCookie]) -> Bool { @@ -229,6 +252,10 @@ public enum OllamaCookieImporter { isRecognizedOllamaSessionCookieName(cookie.name) } } + + private static func accessDeniedDetails(_ hints: [String]) -> String { + Array(Set(hints)).sorted().joined(separator: " ") + } } #endif diff --git a/Sources/CodexBarCore/UsagePace.swift b/Sources/CodexBarCore/UsagePace.swift index c55b54c890..82ddcdde18 100644 --- a/Sources/CodexBarCore/UsagePace.swift +++ b/Sources/CodexBarCore/UsagePace.swift @@ -19,6 +19,14 @@ public struct UsagePace: Sendable { public let willLastToReset: Bool public let runOutProbability: Double? + public var speedMultiplierToReset: Double? { + guard self.actualUsedPercent > 0, + self.expectedUsedPercent > self.actualUsedPercent + else { return nil } + let multiplier = self.expectedUsedPercent / self.actualUsedPercent + return multiplier.isFinite ? multiplier : nil + } + public init( stage: Stage, deltaPercent: Double, diff --git a/Tests/CodexBarTests/CLISnapshotTests.swift b/Tests/CodexBarTests/CLISnapshotTests.swift index f0e4b2a1b9..d6b389eb6d 100644 --- a/Tests/CodexBarTests/CLISnapshotTests.swift +++ b/Tests/CodexBarTests/CLISnapshotTests.swift @@ -490,6 +490,7 @@ struct CLISnapshotTests { #expect(output.contains("Weekly: 77% left")) #expect(output.contains("Pace: 6% in reserve | Expected 29% used | Lasts until reset")) + #expect(!output.contains("Try 1.5x!")) } @Test @@ -545,7 +546,7 @@ struct CLISnapshotTests { #expect(output.contains("Session: 80% left")) // 2h remaining of a 5h window => 3h elapsed => 60% expected; even rate easily lasts to reset. - #expect(output.contains("Pace: 40% in reserve | Expected 60% used | Lasts until reset")) + #expect(output.contains("Pace: 40% in reserve | Expected 60% used | Lasts until reset | Try 1.5x!")) } @Test @@ -573,7 +574,7 @@ struct CLISnapshotTests { now: now) // windowMinutes is nil, so the 5-hour (300 minute) session default must drive the pace. - #expect(output.contains("Pace: 40% in reserve | Expected 60% used")) + #expect(output.contains("Pace: 40% in reserve | Expected 60% used | Lasts until reset | Try 1.5x!")) } @Test diff --git a/Tests/CodexBarTests/SettingsStoreCoverageTests.swift b/Tests/CodexBarTests/SettingsStoreCoverageTests.swift index 22e0724a2f..b56517470d 100644 --- a/Tests/CodexBarTests/SettingsStoreCoverageTests.swift +++ b/Tests/CodexBarTests/SettingsStoreCoverageTests.swift @@ -69,6 +69,13 @@ struct SettingsStoreCoverageTests { #expect(settings.historicalTrackingEnabled == false) settings.historicalTrackingEnabled = true #expect(settings.historicalTrackingEnabled == true) + #expect(settings.historicalPaceMinimumWeeks == 3) + settings.historicalPaceMinimumWeeks = 1 + #expect(settings.historicalPaceMinimumWeeks == 2) + settings.historicalPaceMinimumWeeks = 9 + #expect(settings.historicalPaceMinimumWeeks == 8) + settings.historicalPaceMinimumWeeks = 2 + #expect(settings.historicalPaceMinimumWeeks == 2) settings.resetTimesShowAbsolute = true #expect(settings.resetTimeDisplayStyle == .absolute) diff --git a/Tests/CodexBarTests/UsagePaceTests.swift b/Tests/CodexBarTests/UsagePaceTests.swift index c162bf868c..8aff6681b9 100644 --- a/Tests/CodexBarTests/UsagePaceTests.swift +++ b/Tests/CodexBarTests/UsagePaceTests.swift @@ -42,6 +42,7 @@ struct UsagePaceTests { #expect(pace.etaSeconds == nil) #expect(pace.runOutProbability == nil) #expect(pace.stage == .farBehind) + #expect(abs((pace.speedMultiplierToReset ?? 0) - 8.571) < 0.01) } @Test diff --git a/Tests/CodexBarTests/UsagePaceTextTests.swift b/Tests/CodexBarTests/UsagePaceTextTests.swift index 8eec85007b..88ee6fc443 100644 --- a/Tests/CodexBarTests/UsagePaceTextTests.swift +++ b/Tests/CodexBarTests/UsagePaceTextTests.swift @@ -15,6 +15,7 @@ struct UsagePaceTextTests { "Projected empty in %@", "Runs out now", "Runs out in %@", + "Try 1.5x!", "≈ %d%% run-out risk", "%@ · %@", ] @@ -48,7 +49,7 @@ struct UsagePaceTextTests { let detail = UsagePaceText.weeklyDetail(pace: pace, now: now) #expect(detail.leftLabel == "33% in reserve") - #expect(detail.rightLabel == "Lasts until reset") + #expect(detail.rightLabel == "Lasts until reset · Try 1.5x!") } @Test @@ -66,6 +67,22 @@ struct UsagePaceTextTests { #expect(summary == "Pace: 7% in deficit · Runs out in 3d") } + @Test + func `weekly pace detail reports capped speed headroom when under pace`() throws { + let now = Date(timeIntervalSince1970: 0) + let window = RateWindow( + usedPercent: 20, + windowMinutes: 10080, + resetsAt: now.addingTimeInterval(3 * 24 * 3600), + resetDescription: nil) + let pace = try #require(UsagePace.weekly(window: window, now: now)) + + let detail = UsagePaceText.weeklyDetail(pace: pace, now: now) + + #expect(detail.leftLabel == "37% in reserve") + #expect(detail.rightLabel == "Lasts until reset · Try 1.5x!") + } + @Test func `reported weekly state renders deficit and run out headline`() throws { let now = Date(timeIntervalSince1970: 0) @@ -132,7 +149,7 @@ struct UsagePaceTextTests { let detail = UsagePaceText.weeklyDetail(pace: pace, now: now) - #expect(detail.rightLabel == "Lasts until reset · ≈ 0% run-out risk") + #expect(detail.rightLabel == "Lasts until reset · Try 1.5x! · ≈ 0% run-out risk") } @Test @@ -188,7 +205,7 @@ struct UsagePaceTextTests { #expect(detail != nil) #expect(detail?.leftLabel == "50% in reserve") - #expect(detail?.rightLabel == "Lasts until reset") + #expect(detail?.rightLabel == "Lasts until reset · Try 1.5x!") } @Test From 0af3a7cf70a572ec3186c1c1b15b7053097da9c3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 1 Jul 2026 07:58:15 +0100 Subject: [PATCH 2/6] refactor: keep pace hint scope focused --- .../Providers/Ollama/OllamaUsageFetcher.swift | 59 +++++-------------- 1 file changed, 16 insertions(+), 43 deletions(-) diff --git a/Sources/CodexBarCore/Providers/Ollama/OllamaUsageFetcher.swift b/Sources/CodexBarCore/Providers/Ollama/OllamaUsageFetcher.swift index 984a234a6f..d3aaea9763 100644 --- a/Sources/CodexBarCore/Providers/Ollama/OllamaUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Ollama/OllamaUsageFetcher.swift @@ -37,7 +37,6 @@ public enum OllamaUsageError: LocalizedError, Sendable { case parseFailed(String) case networkError(String) case noSessionCookie - case browserAccessDenied(details: String) public var errorDescription: String? { switch self { @@ -55,8 +54,6 @@ public enum OllamaUsageError: LocalizedError, Sendable { "Ollama request failed: \(message)" case .noSessionCookie: "No Ollama session cookie found. Please log in to ollama.com in your browser." - case let .browserAccessDenied(details): - "Ollama browser cookies are not readable. \(details)" } } } @@ -85,11 +82,6 @@ public enum OllamaCookieImporter { } } - private struct ImportScanResult { - var sessions: [SessionInfo] - var accessDeniedHints: [String] - } - public static func importSessions( browserDetection: BrowserDetection, preferredBrowsers: [Browser] = [.chrome], @@ -97,31 +89,23 @@ public enum OllamaCookieImporter { logger: ((String) -> Void)? = nil) throws -> [SessionInfo] { let log: (String) -> Void = { msg in logger?("[ollama-cookie] \(msg)") } - var accessDeniedHints: [String] = [] let preferredSources = preferredBrowsers.isEmpty ? ollamaCookieImportOrder.cookieImportCandidates(using: browserDetection) : preferredBrowsers.cookieImportCandidates(using: browserDetection) - let preferredResult = self.collectSessionInfo(from: preferredSources, logger: log) - accessDeniedHints.append(contentsOf: preferredResult.accessDeniedHints) - do { - return try self.selectSessionInfosWithFallback( - preferredCandidates: preferredResult.sessions, - allowFallbackBrowsers: allowFallbackBrowsers, - loadFallbackCandidates: { - guard !preferredBrowsers.isEmpty else { return [] } - let fallbackSources = self.fallbackBrowserSources( - browserDetection: browserDetection, - excluding: preferredSources) - guard !fallbackSources.isEmpty else { return [] } - log("No recognized Ollama session in preferred browsers; trying fallback import order") - let fallbackResult = self.collectSessionInfo(from: fallbackSources, logger: log) - accessDeniedHints.append(contentsOf: fallbackResult.accessDeniedHints) - return fallbackResult.sessions - }, - logger: log) - } catch OllamaUsageError.noSessionCookie where !accessDeniedHints.isEmpty { - throw OllamaUsageError.browserAccessDenied(details: Self.accessDeniedDetails(accessDeniedHints)) - } + let preferredCandidates = self.collectSessionInfo(from: preferredSources, logger: log) + return try self.selectSessionInfosWithFallback( + preferredCandidates: preferredCandidates, + allowFallbackBrowsers: allowFallbackBrowsers, + loadFallbackCandidates: { + guard !preferredBrowsers.isEmpty else { return [] } + let fallbackSources = self.fallbackBrowserSources( + browserDetection: browserDetection, + excluding: preferredSources) + guard !fallbackSources.isEmpty else { return [] } + log("No recognized Ollama session in preferred browsers; trying fallback import order") + return self.collectSessionInfo(from: fallbackSources, logger: log) + }, + logger: log) } public static func importSession( @@ -217,10 +201,9 @@ public enum OllamaCookieImporter { private static func collectSessionInfo( from browserSources: [Browser], - logger: @escaping (String) -> Void) -> ImportScanResult + logger: @escaping (String) -> Void) -> [SessionInfo] { var candidates: [SessionInfo] = [] - var accessDeniedHints: [String] = [] for browserSource in browserSources { do { let query = BrowserCookieQuery(domains: self.cookieDomains) @@ -233,18 +216,12 @@ public enum OllamaCookieImporter { guard !cookies.isEmpty else { continue } candidates.append(SessionInfo(cookies: cookies, sourceLabel: source.label)) } - } catch let error as BrowserCookieError { - BrowserCookieAccessGate.recordIfNeeded(error) - if let hint = error.accessDeniedHint { - accessDeniedHints.append(hint) - } - logger("\(browserSource.displayName) cookie import failed: \(error.localizedDescription)") } catch { BrowserCookieAccessGate.recordIfNeeded(error) logger("\(browserSource.displayName) cookie import failed: \(error.localizedDescription)") } } - return ImportScanResult(sessions: candidates, accessDeniedHints: accessDeniedHints) + return candidates } private static func containsRecognizedSessionCookie(in cookies: [HTTPCookie]) -> Bool { @@ -252,10 +229,6 @@ public enum OllamaCookieImporter { isRecognizedOllamaSessionCookieName(cookie.name) } } - - private static func accessDeniedDetails(_ hints: [String]) -> String { - Array(Set(hints)).sorted().joined(separator: " ") - } } #endif From 22f854b5e8a6cfb5280ab2bd18c13f93298c5c9d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 1 Jul 2026 08:09:59 +0100 Subject: [PATCH 3/6] style: format pace settings --- Sources/CodexBar/PreferencesAdvancedPane.swift | 4 +++- Sources/CodexBarCLI/CLIRenderer.swift | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/CodexBar/PreferencesAdvancedPane.swift b/Sources/CodexBar/PreferencesAdvancedPane.swift index f686165e5e..0c945a39be 100644 --- a/Sources/CodexBar/PreferencesAdvancedPane.swift +++ b/Sources/CodexBar/PreferencesAdvancedPane.swift @@ -83,7 +83,9 @@ struct AdvancedPane: View { } Spacer(minLength: 12) Stepper(value: self.$settings.historicalPaceMinimumWeeks, in: 2...8, step: 1) { - Text(String(format: L("historical_pace_weeks_value"), self.settings.historicalPaceMinimumWeeks)) + Text(String( + format: L("historical_pace_weeks_value"), + self.settings.historicalPaceMinimumWeeks)) .font(.footnote) .foregroundStyle(.secondary) .monospacedDigit() diff --git a/Sources/CodexBarCLI/CLIRenderer.swift b/Sources/CodexBarCLI/CLIRenderer.swift index 0e4aa90bd5..41f92809cc 100644 --- a/Sources/CodexBarCLI/CLIRenderer.swift +++ b/Sources/CodexBarCLI/CLIRenderer.swift @@ -572,7 +572,7 @@ enum CLIRenderer { } private static func paceRightLabel(for pace: UsagePace, kind: PaceKind, now: Date) -> String? { - if pace.willLastToReset { return Self.combinedLastsLabel(for: pace) } + if pace.willLastToReset { return self.combinedLastsLabel(for: pace) } guard let etaSeconds = pace.etaSeconds else { return nil } let etaText = Self.paceDurationText(seconds: etaSeconds, now: now) switch kind { @@ -584,7 +584,7 @@ enum CLIRenderer { } private static func combinedLastsLabel(for pace: UsagePace) -> String { - guard let speedLabel = Self.speedHintLabel(for: pace) else { + guard let speedLabel = speedHintLabel(for: pace) else { return "Lasts until reset" } return "Lasts until reset | \(speedLabel)" From e1bb46e7bf10f5819f384e295d215665f5b3ee99 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 1 Jul 2026 08:15:23 +0100 Subject: [PATCH 4/6] fix: localize Italian pace settings --- Sources/CodexBar/Resources/it.lproj/Localizable.strings | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/CodexBar/Resources/it.lproj/Localizable.strings b/Sources/CodexBar/Resources/it.lproj/Localizable.strings index 72287af2a6..7f337066b8 100644 --- a/Sources/CodexBar/Resources/it.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/it.lproj/Localizable.strings @@ -549,9 +549,9 @@ "no_writable_bin_dirs" = "Nessuna directory bin scrivibile trovata."; "show_debug_settings_title" = "Mostra impostazioni debug"; "show_debug_settings_subtitle" = "Espone strumenti di diagnosi nella scheda Debug."; -"historical_pace_weeks_title" = "Historical pace weeks"; -"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; -"historical_pace_weeks_value" = "%d weeks"; +"historical_pace_weeks_title" = "Settimane per il ritmo storico"; +"historical_pace_weeks_subtitle" = "Numero minimo di settimane Codex complete prima che il ritmo storico sostituisca quello lineare."; +"historical_pace_weeks_value" = "%d settimane"; "surprise_me_title" = "Sorprendimi"; "surprise_me_subtitle" = "Per chi apprezza un po' di personalità dagli agenti."; "weekly_limit_confetti_title" = "Coriandoli limite settimanale"; @@ -729,7 +729,7 @@ "Resets now" = "Si resetta ora"; "reset_tomorrow_format" = "domani, %@"; "Lasts until reset" = "Valido fino al reset"; -"Try 1.5x!" = "Try 1.5x!"; +"Try 1.5x!" = "Prova 1,5x!"; "Updated %@" = "Aggiornato %@"; "Updated %@h ago" = "Aggiornato %@ h fa"; "Updated %@m ago" = "Aggiornato %@ min fa"; From d6a732cb6f895975b47f63b0437fb18bcd9e8db9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 1 Jul 2026 08:25:25 +0100 Subject: [PATCH 5/6] fix: compute pace headroom from remaining burn --- Sources/CodexBar/HistoricalUsagePace.swift | 3 +- Sources/CodexBarCore/UsagePace.swift | 43 ++++++++++++++------ Tests/CodexBarTests/UsagePaceTests.swift | 31 +++++++++++++- Tests/CodexBarTests/UsagePaceTextTests.swift | 19 ++++++++- 4 files changed, 80 insertions(+), 16 deletions(-) diff --git a/Sources/CodexBar/HistoricalUsagePace.swift b/Sources/CodexBar/HistoricalUsagePace.swift index 4e75d4da67..37d8b74e26 100644 --- a/Sources/CodexBar/HistoricalUsagePace.swift +++ b/Sources/CodexBar/HistoricalUsagePace.swift @@ -898,7 +898,8 @@ enum CodexHistoricalPaceEvaluator { actualUsedPercent: actual, etaSeconds: etaSeconds, willLastToReset: willLastToReset, - runOutProbability: runOutProbability) + runOutProbability: runOutProbability, + projectedRemainingUsage: max(0, (expectedCurve.last ?? expectedNow) - expectedNow)) } private static func firstCrossing( diff --git a/Sources/CodexBarCore/UsagePace.swift b/Sources/CodexBarCore/UsagePace.swift index 82ddcdde18..8f55aa4475 100644 --- a/Sources/CodexBarCore/UsagePace.swift +++ b/Sources/CodexBarCore/UsagePace.swift @@ -18,14 +18,7 @@ public struct UsagePace: Sendable { public let etaSeconds: TimeInterval? public let willLastToReset: Bool public let runOutProbability: Double? - - public var speedMultiplierToReset: Double? { - guard self.actualUsedPercent > 0, - self.expectedUsedPercent > self.actualUsedPercent - else { return nil } - let multiplier = self.expectedUsedPercent / self.actualUsedPercent - return multiplier.isFinite ? multiplier : nil - } + public let speedMultiplierToReset: Double? public init( stage: Stage, @@ -34,7 +27,8 @@ public struct UsagePace: Sendable { actualUsedPercent: Double, etaSeconds: TimeInterval?, willLastToReset: Bool, - runOutProbability: Double? = nil) + runOutProbability: Double? = nil, + speedMultiplierToReset: Double? = nil) { self.stage = stage self.deltaPercent = deltaPercent @@ -43,6 +37,7 @@ public struct UsagePace: Sendable { self.etaSeconds = etaSeconds self.willLastToReset = willLastToReset self.runOutProbability = runOutProbability + self.speedMultiplierToReset = speedMultiplierToReset } public static func weekly( @@ -86,6 +81,13 @@ public struct UsagePace: Sendable { var willLastToReset = false let paceElapsed = workdayProgress?.elapsedSeconds ?? elapsed + let effectiveTimeUntilReset = workdayProgress?.remainingSeconds ?? timeUntilReset + let projectedRemainingUsage = paceElapsed > 0 + ? actual * effectiveTimeUntilReset / paceElapsed + : 0 + let speedMultiplierToReset = Self.safeSpeedMultiplier( + remainingCapacity: 100 - actual, + projectedRemainingUsage: projectedRemainingUsage) if actual >= 100 { etaSeconds = 0 } else if paceElapsed > 0, actual > 0 { @@ -93,7 +95,6 @@ public struct UsagePace: Sendable { if rate > 0 { let remaining = 100 - actual let candidate = remaining / rate - let effectiveTimeUntilReset = workdayProgress?.remainingSeconds ?? timeUntilReset if candidate >= effectiveTimeUntilReset { willLastToReset = true } else if let workDays = workdayProgress?.workDays { @@ -118,7 +119,8 @@ public struct UsagePace: Sendable { actualUsedPercent: actual, etaSeconds: etaSeconds, willLastToReset: willLastToReset, - runOutProbability: nil) + runOutProbability: nil, + speedMultiplierToReset: speedMultiplierToReset) } public static func historical( @@ -126,7 +128,8 @@ public struct UsagePace: Sendable { actualUsedPercent: Double, etaSeconds: TimeInterval?, willLastToReset: Bool, - runOutProbability: Double?) -> UsagePace + runOutProbability: Double?, + projectedRemainingUsage: Double? = nil) -> UsagePace { let expected = expectedUsedPercent.clamped(to: 0...100) let actual = actualUsedPercent.clamped(to: 0...100) @@ -138,7 +141,21 @@ public struct UsagePace: Sendable { actualUsedPercent: actual, etaSeconds: etaSeconds, willLastToReset: willLastToReset, - runOutProbability: runOutProbability) + runOutProbability: runOutProbability, + speedMultiplierToReset: projectedRemainingUsage.flatMap { + Self.safeSpeedMultiplier( + remainingCapacity: 100 - actual, + projectedRemainingUsage: $0) + }) + } + + private static func safeSpeedMultiplier( + remainingCapacity: Double, + projectedRemainingUsage: Double) -> Double? + { + guard remainingCapacity > 0, projectedRemainingUsage > 0 else { return nil } + let multiplier = remainingCapacity / projectedRemainingUsage + return multiplier.isFinite ? multiplier : nil } private struct WorkdayProgress { diff --git a/Tests/CodexBarTests/UsagePaceTests.swift b/Tests/CodexBarTests/UsagePaceTests.swift index 8aff6681b9..a5b3f78b2f 100644 --- a/Tests/CodexBarTests/UsagePaceTests.swift +++ b/Tests/CodexBarTests/UsagePaceTests.swift @@ -42,7 +42,36 @@ struct UsagePaceTests { #expect(pace.etaSeconds == nil) #expect(pace.runOutProbability == nil) #expect(pace.stage == .farBehind) - #expect(abs((pace.speedMultiplierToReset ?? 0) - 8.571) < 0.01) + #expect(abs((pace.speedMultiplierToReset ?? 0) - 14.25) < 0.01) + } + + @Test + func `weekly pace speed headroom uses remaining burn capacity`() throws { + let now = Date(timeIntervalSince1970: 0) + let window = RateWindow( + usedPercent: 70, + windowMinutes: 10080, + resetsAt: now.addingTimeInterval(0.7 * 24 * 3600), + resetDescription: nil) + + let pace = try #require(UsagePace.weekly(window: window, now: now)) + + #expect(abs(pace.expectedUsedPercent - 90) < 0.01) + #expect(pace.willLastToReset) + #expect(abs((pace.speedMultiplierToReset ?? 0) - 3.857) < 0.01) + } + + @Test + func `historical pace speed headroom uses projected remaining usage`() { + let pace = UsagePace.historical( + expectedUsedPercent: 45, + actualUsedPercent: 20, + etaSeconds: nil, + willLastToReset: true, + runOutProbability: 0, + projectedRemainingUsage: 20) + + #expect(pace.speedMultiplierToReset == 4) } @Test diff --git a/Tests/CodexBarTests/UsagePaceTextTests.swift b/Tests/CodexBarTests/UsagePaceTextTests.swift index 88ee6fc443..bb55af5b89 100644 --- a/Tests/CodexBarTests/UsagePaceTextTests.swift +++ b/Tests/CodexBarTests/UsagePaceTextTests.swift @@ -83,6 +83,22 @@ struct UsagePaceTextTests { #expect(detail.rightLabel == "Lasts until reset · Try 1.5x!") } + @Test + func `weekly pace detail reports remaining headroom late in window`() throws { + let now = Date(timeIntervalSince1970: 0) + let window = RateWindow( + usedPercent: 70, + windowMinutes: 10080, + resetsAt: now.addingTimeInterval(0.7 * 24 * 3600), + resetDescription: nil) + let pace = try #require(UsagePace.weekly(window: window, now: now)) + + let detail = UsagePaceText.weeklyDetail(pace: pace, now: now) + + #expect(detail.leftLabel == "20% in reserve") + #expect(detail.rightLabel == "Lasts until reset · Try 1.5x!") + } + @Test func `reported weekly state renders deficit and run out headline`() throws { let now = Date(timeIntervalSince1970: 0) @@ -145,7 +161,8 @@ struct UsagePaceTextTests { actualUsedPercent: 10, etaSeconds: nil, willLastToReset: true, - runOutProbability: 0.02) + runOutProbability: 0.02, + speedMultiplierToReset: 4) let detail = UsagePaceText.weeklyDetail(pace: pace, now: now) From d7a997d9e8267c63b78ca28f2553930b35694d19 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 3 Jul 2026 13:10:44 -0700 Subject: [PATCH 6/6] refactor: focus Codex pace headroom Co-authored-by: astuteprogrammer --- CHANGELOG.md | 3 + Sources/CodexBar/HistoricalUsagePace.swift | 11 +--- .../CodexBar/MenuCardView+ModelHelpers.swift | 5 +- Sources/CodexBar/MenuCardView.swift | 3 + Sources/CodexBar/MenuDescriptor.swift | 4 +- .../Resources/ar.lproj/Localizable.strings | 5 +- .../Resources/ca.lproj/Localizable.strings | 5 +- .../Resources/de.lproj/Localizable.strings | 2 +- .../Resources/en.lproj/Localizable.strings | 5 +- .../Resources/es.lproj/Localizable.strings | 4 +- .../Resources/fa.lproj/Localizable.strings | 5 +- .../Resources/fr.lproj/Localizable.strings | 5 +- .../Resources/id.lproj/Localizable.strings | 5 +- .../Resources/it.lproj/Localizable.strings | 5 +- .../Resources/ja.lproj/Localizable.strings | 5 +- .../Resources/ko.lproj/Localizable.strings | 5 +- .../Resources/nl.lproj/Localizable.strings | 5 +- .../Resources/pl.lproj/Localizable.strings | 5 +- .../Resources/pt-BR.lproj/Localizable.strings | 5 +- .../Resources/sv.lproj/Localizable.strings | 5 +- .../Resources/th.lproj/Localizable.strings | 5 +- .../Resources/tr.lproj/Localizable.strings | 5 +- .../Resources/uk.lproj/Localizable.strings | 5 +- .../Resources/vi.lproj/Localizable.strings | 5 +- .../zh-Hans.lproj/Localizable.strings | 5 +- .../zh-Hant.lproj/Localizable.strings | 4 +- Sources/CodexBar/SettingsStore+Defaults.swift | 9 --- .../SettingsStore+MenuObservation.swift | 1 - Sources/CodexBar/SettingsStore.swift | 3 - Sources/CodexBar/SettingsStoreState.swift | 1 - Sources/CodexBar/UsagePaceText.swift | 24 ++++--- .../CodexBar/UsageStore+HistoricalPace.swift | 3 +- Sources/CodexBarCLI/CLIRenderer.swift | 27 +++++--- Tests/CodexBarTests/CLISnapshotTests.swift | 7 +- .../HistoricalUsagePaceTests.swift | 4 +- .../SettingsStoreCoverageTests.swift | 7 -- Tests/CodexBarTests/UsagePaceTextTests.swift | 65 ++++++++++++++----- 37 files changed, 124 insertions(+), 153 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce6f3c9098..a1961a6428 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 0.38.1 — Unreleased +### Added +- Codex: show a conservative 1.5× pace-headroom hint in menus and CLI output when usage is safely ahead of the reset curve. Thanks @astuteprogrammer! + ### Changed - Website: redesign codexbar.app around faster download, provider discovery, feature, CLI, and widget paths with responsive dark/light and localized layouts. Thanks @vyctorbrzezowski! diff --git a/Sources/CodexBar/HistoricalUsagePace.swift b/Sources/CodexBar/HistoricalUsagePace.swift index 37d8b74e26..70a6bdb233 100644 --- a/Sources/CodexBar/HistoricalUsagePace.swift +++ b/Sources/CodexBar/HistoricalUsagePace.swift @@ -758,18 +758,13 @@ actor HistoricalUsageHistoryStore { } enum CodexHistoricalPaceEvaluator { - static let defaultMinimumCompleteWeeksForHistorical = 3 + static let minimumCompleteWeeksForHistorical = 3 static let minimumWeeksForRisk = 5 private static let recencyTauWeeks: Double = 3 private static let epsilon: Double = 1e-9 private static let resetBucketSeconds: TimeInterval = 5 * 60 - static func evaluate( - window: RateWindow, - now: Date, - dataset: CodexHistoricalDataset?, - minimumCompleteWeeks: Int = Self.defaultMinimumCompleteWeeksForHistorical) -> UsagePace? - { + static func evaluate(window: RateWindow, now: Date, dataset: CodexHistoricalDataset?) -> UsagePace? { guard let dataset else { return nil } guard let resetsAt = window.resetsAt else { return nil } let minutes = window.windowMinutes ?? 10080 @@ -790,7 +785,7 @@ enum CodexHistoricalPaceEvaluator { let scopedWeeks = dataset.weeks.filter { week in week.windowMinutes == minutes && week.resetsAt < normalizedResetsAt } - guard scopedWeeks.count >= max(2, minimumCompleteWeeks) else { return nil } + guard scopedWeeks.count >= Self.minimumCompleteWeeksForHistorical else { return nil } let weightedWeeks = scopedWeeks.map { week in let ageWeeks = Self.clamp( diff --git a/Sources/CodexBar/MenuCardView+ModelHelpers.swift b/Sources/CodexBar/MenuCardView+ModelHelpers.swift index 02123b776a..ff2a3153e5 100644 --- a/Sources/CodexBar/MenuCardView+ModelHelpers.swift +++ b/Sources/CodexBar/MenuCardView+ModelHelpers.swift @@ -373,13 +373,14 @@ extension UsageMenuCardView.Model { } static func weeklyPaceDetail( + provider: UsageProvider, window: RateWindow, now: Date, pace: UsagePace?, showUsed: Bool) -> PaceDetail? { guard let pace else { return nil } - let detail = UsagePaceText.weeklyDetail(pace: pace, now: now) + let detail = UsagePaceText.weeklyDetail(provider: provider, pace: pace, now: now) let expectedUsed = detail.expectedUsedPercent let actualUsed = window.usedPercent let expectedPercent = showUsed ? expectedUsed : (100 - expectedUsed) @@ -426,6 +427,7 @@ extension UsageMenuCardView.Model { workDays: input.workDaysPerWeek) guard let resolved = Self.displayableWeeklyPace(resolved) else { return nil } return Self.weeklyPaceDetail( + provider: input.provider, window: window, now: input.now, pace: resolved, @@ -591,6 +593,7 @@ extension UsageMenuCardView.Model { defaultWindowMinutes: 10080, workDays: input.workDaysPerWeek)) return Self.weeklyPaceDetail( + provider: provider, window: window, now: input.now, pace: pace, diff --git a/Sources/CodexBar/MenuCardView.swift b/Sources/CodexBar/MenuCardView.swift index cb8c5282e9..4afe1f7ff1 100644 --- a/Sources/CodexBar/MenuCardView.swift +++ b/Sources/CodexBar/MenuCardView.swift @@ -1305,6 +1305,7 @@ extension UsageMenuCardView.Model { } if let pace = input.weeklyPace { let paceDetail = Self.weeklyPaceDetail( + provider: input.provider, window: primary, now: input.now, pace: pace, @@ -1373,6 +1374,7 @@ extension UsageMenuCardView.Model { zaiTimeDetail: String?) -> Metric { var paceDetail = Self.weeklyPaceDetail( + provider: input.provider, window: weekly, now: input.now, pace: input.weeklyPace, @@ -1495,6 +1497,7 @@ extension UsageMenuCardView.Model { title = L(input.metadata.weeklyLabel) id = "secondary" paceDetail = Self.weeklyPaceDetail( + provider: input.provider, window: window, now: input.now, pace: Self.standardWeeklyPace(input: input, window: window), diff --git a/Sources/CodexBar/MenuDescriptor.swift b/Sources/CodexBar/MenuDescriptor.swift index af1f551a98..2441f11915 100644 --- a/Sources/CodexBar/MenuDescriptor.swift +++ b/Sources/CodexBar/MenuDescriptor.swift @@ -187,7 +187,7 @@ struct MenuDescriptor { if provider == .abacus, let pace = store.weeklyPace(provider: provider, window: primary) { - let paceSummary = UsagePaceText.weeklySummary(pace: pace) + let paceSummary = UsagePaceText.weeklySummary(provider: provider, pace: pace) entries.append(.text(paceSummary, .secondary)) } if let paceSummary = UsagePaceText.sessionSummary(provider: provider, window: primary) { @@ -220,7 +220,7 @@ struct MenuDescriptor { entries.append(.text(detail, .secondary)) } if let pace = store.weeklyPace(provider: provider, window: weekly) { - let paceSummary = UsagePaceText.weeklySummary(pace: pace) + let paceSummary = UsagePaceText.weeklySummary(provider: provider, pace: pace) entries.append(.text(paceSummary, .secondary)) } } diff --git a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings index 785d43f6c6..396340ad5d 100644 --- a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings @@ -553,9 +553,6 @@ "no_writable_bin_dirs" = "لم يتم العثور على أي سجلات قابلة للكتابة."; "show_debug_settings_title" = "عرض إعدادات التصحيح"; "show_debug_settings_subtitle" = "اعرض أدوات استكشاف الأخطاء في تبويب التصحيح."; -"historical_pace_weeks_title" = "Historical pace weeks"; -"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; -"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "فاجئني"; "surprise_me_subtitle" = "تحقق إذا كنت تحب وكلائنك يستمتعون هناك."; "session_limit_confetti_title" = "قصاصات ورقية لحد الجلسة"; @@ -735,7 +732,7 @@ "Resets now" = "إعادة التعيين الآن"; "reset_tomorrow_format" = "غدًا، %@"; "Lasts until reset" = "يستمر حتى إعادة التعيين"; -"Try 1.5x!" = "Try 1.5x!"; +"1.5× headroom" = "هامش 1.5×"; "Updated %@" = "تحديث %@"; "Updated relative %@" = "تحديث %@"; "Updated absolute %@" = "تحديث %@"; diff --git a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings index ec857cb6e8..6ec3b24984 100644 --- a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings @@ -539,9 +539,6 @@ "no_writable_bin_dirs" = "No s'han trobat directoris bin amb permís d'escriptura."; "show_debug_settings_title" = "Mostreu la configuració de depuració"; "show_debug_settings_subtitle" = "Mostreu eines de diagnòstic a la pestanya Depuració."; -"historical_pace_weeks_title" = "Historical pace weeks"; -"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; -"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Sorprèn-me"; "surprise_me_subtitle" = "Activeu-ho si us agrada que els vostres agents es diverteixin allà dalt."; "session_limit_confetti_title" = "Confeti del límit de sessió"; @@ -1038,7 +1035,7 @@ "Last 30 days: %@" = "Últims 30 dies: %@"; "Last 30 days: %@ · %@ tokens" = "Últims 30 dies: %@ · %@ tokens"; "Lasts until reset" = "Dura fins al reinici"; -"Try 1.5x!" = "Try 1.5x!"; +"1.5× headroom" = "marge d’1,5×"; "Login with Google" = "Inicieu sessió amb Google"; "login_success_notification_body" = "Podeu tornar a l'app; l'autenticació ha finalitzat."; "login_success_notification_title" = "Inici de sessió de %@ correcte"; diff --git a/Sources/CodexBar/Resources/de.lproj/Localizable.strings b/Sources/CodexBar/Resources/de.lproj/Localizable.strings index 94e134f416..cd071a3c9f 100644 --- a/Sources/CodexBar/Resources/de.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/de.lproj/Localizable.strings @@ -729,7 +729,7 @@ "Resets now" = "Wird jetzt zurückgesetzt"; "reset_tomorrow_format" = "morgen, %@"; "Lasts until reset" = "Hält bis zum Zurücksetzen an"; -"Try 1.5x!" = "Try 1.5x!"; +"1.5× headroom" = "1,5× Spielraum"; "Updated %@" = "Aktualisiert %@"; "Updated relative %@" = "Aktualisiert %@"; "Updated absolute %@" = "Aktualisiert %@"; diff --git a/Sources/CodexBar/Resources/en.lproj/Localizable.strings b/Sources/CodexBar/Resources/en.lproj/Localizable.strings index a46ba2a562..2da28251f6 100644 --- a/Sources/CodexBar/Resources/en.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/en.lproj/Localizable.strings @@ -553,9 +553,6 @@ "no_writable_bin_dirs" = "No writable bin dirs found."; "show_debug_settings_title" = "Show Debug Settings"; "show_debug_settings_subtitle" = "Expose troubleshooting tools in the Debug tab."; -"historical_pace_weeks_title" = "Historical pace weeks"; -"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; -"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Surprise me"; "surprise_me_subtitle" = "Check if you like your agents having some fun up there."; "session_limit_confetti_title" = "Session limit confetti"; @@ -735,7 +732,7 @@ "Resets now" = "Resets now"; "reset_tomorrow_format" = "tomorrow, %@"; "Lasts until reset" = "Lasts until reset"; -"Try 1.5x!" = "Try 1.5x!"; +"1.5× headroom" = "1.5× headroom"; "Updated %@" = "Updated %@"; "Updated relative %@" = "Updated %@"; "Updated absolute %@" = "Updated %@"; diff --git a/Sources/CodexBar/Resources/es.lproj/Localizable.strings b/Sources/CodexBar/Resources/es.lproj/Localizable.strings index f76e596b3c..30064137d8 100644 --- a/Sources/CodexBar/Resources/es.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/es.lproj/Localizable.strings @@ -542,9 +542,7 @@ "no_writable_bin_dirs" = "No se encontraron directorios bin con permiso de escritura."; "show_debug_settings_title" = "Mostrar ajustes de depuración"; "show_debug_settings_subtitle" = "Muestra herramientas de diagnóstico en la pestaña Depuración."; -"historical_pace_weeks_title" = "Historical pace weeks"; -"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; -"historical_pace_weeks_value" = "%d weeks"; +"1.5× headroom" = "margen de 1,5×"; "surprise_me_title" = "Sorpréndeme"; "surprise_me_subtitle" = "Actívalo si te gusta que tus agentes se diviertan ahí arriba."; "session_limit_confetti_title" = "Confeti del límite de sesión"; diff --git a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings index dcbb95df0c..c7bf07f936 100644 --- a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings @@ -553,9 +553,6 @@ "no_writable_bin_dirs" = "هیچ سطل قابل نوشتاری پیدا نشد."; "show_debug_settings_title" = "نمایش تنظیمات اشکال زدایی"; "show_debug_settings_subtitle" = "ابزارهای عیب یابی را در تب اشکال زدایی آشکار کنید."; -"historical_pace_weeks_title" = "Historical pace weeks"; -"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; -"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "سورپرایزم کن"; "surprise_me_subtitle" = "بررسی کن که آیا دوست داری مأمورانت آنجا خوش بگذرانند یا نه."; "session_limit_confetti_title" = "کاغذرنگی حد جلسه"; @@ -735,7 +732,7 @@ "Resets now" = "اکنون بازنشانی می شود"; "reset_tomorrow_format" = "فردا، %@"; "Lasts until reset" = "تا زمان ریست ادامه دارد"; -"Try 1.5x!" = "Try 1.5x!"; +"1.5× headroom" = "حاشیه ۱٫۵×"; "Updated %@" = "به روزرسانی %@"; "Updated relative %@" = "به روزرسانی %@"; "Updated absolute %@" = "به روزرسانی %@"; diff --git a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings index 1a8321d44f..b70ae0a913 100644 --- a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings @@ -550,9 +550,6 @@ "no_writable_bin_dirs" = "Aucun répertoire bin inscriptible trouvé."; "show_debug_settings_title" = "Afficher les paramètres de débogage"; "show_debug_settings_subtitle" = "Exposez les outils de dépannage dans l’onglet Débogage."; -"historical_pace_weeks_title" = "Historical pace weeks"; -"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; -"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Surprenez-moi"; "surprise_me_subtitle" = "Vérifiez si vous aimez que vos agents s'amusent là-haut."; "session_limit_confetti_title" = "Confettis de limite de session"; @@ -732,7 +729,7 @@ "Resets now" = "Réinitialise maintenant"; "reset_tomorrow_format" = "demain, %@"; "Lasts until reset" = "Dure jusqu'à la réinitialisation"; -"Try 1.5x!" = "Try 1.5x!"; +"1.5× headroom" = "marge de 1,5×"; "Updated %@" = "%@ mis à jour"; "Updated relative %@" = "%@ mis à jour"; "Updated absolute %@" = "%@ mis à jour"; diff --git a/Sources/CodexBar/Resources/id.lproj/Localizable.strings b/Sources/CodexBar/Resources/id.lproj/Localizable.strings index 55f25e3e4a..220bffd67b 100644 --- a/Sources/CodexBar/Resources/id.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/id.lproj/Localizable.strings @@ -555,9 +555,6 @@ "no_writable_bin_dirs" = "Tidak ada direktori bin yang dapat ditulis."; "show_debug_settings_title" = "Tampilkan Pengaturan Debug"; "show_debug_settings_subtitle" = "Tampilkan alat pemecahan masalah di tab Debug."; -"historical_pace_weeks_title" = "Historical pace weeks"; -"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; -"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Kejutkan saya"; "surprise_me_subtitle" = "Centang jika Anda suka agen bersenang-senang di atas sana."; "session_limit_confetti_title" = "Konfeti batas sesi"; @@ -737,7 +734,7 @@ "Resets now" = "Reset sekarang"; "reset_tomorrow_format" = "besok, %@"; "Lasts until reset" = "Bertahan hingga reset"; -"Try 1.5x!" = "Try 1.5x!"; +"1.5× headroom" = "ruang 1,5×"; "Updated %@" = "Diperbarui %@"; "Updated relative %@" = "Diperbarui %@"; "Updated absolute %@" = "Diperbarui %@"; diff --git a/Sources/CodexBar/Resources/it.lproj/Localizable.strings b/Sources/CodexBar/Resources/it.lproj/Localizable.strings index 8d2e8c7266..9fb4229983 100644 --- a/Sources/CodexBar/Resources/it.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/it.lproj/Localizable.strings @@ -555,9 +555,6 @@ "no_writable_bin_dirs" = "Nessuna directory bin scrivibile trovata."; "show_debug_settings_title" = "Mostra impostazioni debug"; "show_debug_settings_subtitle" = "Espone strumenti di diagnosi nella scheda Debug."; -"historical_pace_weeks_title" = "Settimane per il ritmo storico"; -"historical_pace_weeks_subtitle" = "Numero minimo di settimane Codex complete prima che il ritmo storico sostituisca quello lineare."; -"historical_pace_weeks_value" = "%d settimane"; "surprise_me_title" = "Sorprendimi"; "surprise_me_subtitle" = "Per chi apprezza un po' di personalità dagli agenti."; "session_limit_confetti_title" = "Coriandoli limite sessione"; @@ -737,7 +734,7 @@ "Resets now" = "Si resetta ora"; "reset_tomorrow_format" = "domani, %@"; "Lasts until reset" = "Valido fino al reset"; -"Try 1.5x!" = "Prova 1,5x!"; +"1.5× headroom" = "margine 1,5×"; "Updated %@" = "Aggiornato %@"; "Updated relative %@" = "Aggiornato %@"; "Updated absolute %@" = "Aggiornato %@"; diff --git a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings index 372d51bfb8..b8e1684b70 100644 --- a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings @@ -547,9 +547,6 @@ "no_writable_bin_dirs" = "書き込み可能な bin ディレクトリが見つかりません。"; "show_debug_settings_title" = "デバッグ設定を表示"; "show_debug_settings_subtitle" = "デバッグタブにトラブルシューティングツールを表示します。"; -"historical_pace_weeks_title" = "Historical pace weeks"; -"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; -"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "サプライズ"; "surprise_me_subtitle" = "エージェントたちがメニューバーで少し遊ぶのが好きか試してみてください。"; "session_limit_confetti_title" = "セッション上限の紙吹雪"; @@ -729,7 +726,7 @@ "Resets now" = "まもなくリセット"; "reset_tomorrow_format" = "明日 %@"; "Lasts until reset" = "リセットまで持続"; -"Try 1.5x!" = "Try 1.5x!"; +"1.5× headroom" = "1.5倍の余裕"; "Updated %@" = "%@ に更新"; "Updated relative %@" = "%@ に更新"; "Updated absolute %@" = "%@ に更新"; diff --git a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings index ab14485d9e..98e7dc4eb3 100644 --- a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings @@ -539,9 +539,6 @@ "no_writable_bin_dirs" = "쓰기 가능한 bin 디렉터리를 찾을 수 없습니다."; "show_debug_settings_title" = "디버그 설정 표시"; "show_debug_settings_subtitle" = "디버그 탭에 문제 해결 도구를 표시합니다."; -"historical_pace_weeks_title" = "Historical pace weeks"; -"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; -"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "깜짝 놀래주기"; "surprise_me_subtitle" = "에이전트가 저 위에서 조금 즐기는 모습이 마음에 드는지 확인해 보세요."; "session_limit_confetti_title" = "세션 한도 색종이"; @@ -705,7 +702,7 @@ "Resets now" = "지금 재설정"; "reset_tomorrow_format" = "내일 %@"; "Lasts until reset" = "재설정까지 유지"; -"Try 1.5x!" = "Try 1.5x!"; +"1.5× headroom" = "1.5배 여유"; "Updated %@" = "%@에 업데이트됨"; "Updated relative %@" = "%@에 업데이트됨"; "Updated absolute %@" = "%@에 업데이트됨"; diff --git a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings index 8611aadb86..8eea6612ef 100644 --- a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings @@ -550,9 +550,6 @@ "no_writable_bin_dirs" = "Geen beschrijfbare mapmap gevonden."; "show_debug_settings_title" = "Toon foutopsporingsinstellingen"; "show_debug_settings_subtitle" = "Geef hulpprogramma's voor probleemoplossing weer op het tabblad Foutopsporing."; -"historical_pace_weeks_title" = "Historical pace weeks"; -"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; -"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Verras mij"; "surprise_me_subtitle" = "Controleer of je het leuk vindt dat je agenten daar plezier hebben."; "session_limit_confetti_title" = "Confetti bij sessielimiet"; @@ -732,7 +729,7 @@ "Resets now" = "Wordt nu gereset"; "reset_tomorrow_format" = "morgen, %@"; "Lasts until reset" = "Gaat mee tot reset"; -"Try 1.5x!" = "Try 1.5x!"; +"1.5× headroom" = "1,5× speelruimte"; "Updated %@" = "Bijgewerkt %@"; "Updated relative %@" = "Bijgewerkt %@"; "Updated absolute %@" = "Bijgewerkt %@"; diff --git a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings index 59052660f3..44f59596f2 100644 --- a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings @@ -555,9 +555,6 @@ "no_writable_bin_dirs" = "Nie znaleziono zapisywalnych katalogów bin."; "show_debug_settings_title" = "Pokaż ustawienia debugowania"; "show_debug_settings_subtitle" = "Pokazuje dodatkowe ustawienia debugowania i diagnostyki."; -"historical_pace_weeks_title" = "Historical pace weeks"; -"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; -"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Zaskocz mnie"; "surprise_me_subtitle" = "Sprawdź, czy lubisz, gdy twoi agenci trochę się tam bawią."; "session_limit_confetti_title" = "Konfetti limitu sesji"; @@ -737,7 +734,7 @@ "Resets now" = "Reset teraz"; "reset_tomorrow_format" = "jutro, %@"; "Lasts until reset" = "Wystarcza do resetu"; -"Try 1.5x!" = "Try 1.5x!"; +"1.5× headroom" = "zapas 1,5×"; "Updated %@" = "Zaktualizowano %@"; "Updated relative %@" = "Zaktualizowano %@"; "Updated absolute %@" = "Zaktualizowano %@"; diff --git a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings index fe5cbab33f..2bf66e3f19 100644 --- a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings @@ -547,9 +547,6 @@ "no_writable_bin_dirs" = "Nenhum diretório bin gravável encontrado."; "show_debug_settings_title" = "Mostrar ajustes de depuração"; "show_debug_settings_subtitle" = "Exibe ferramentas de diagnóstico na aba Depuração."; -"historical_pace_weeks_title" = "Historical pace weeks"; -"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; -"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Surpreenda-me"; "surprise_me_subtitle" = "Veja se você gosta dos seus agentes se divertindo ali em cima."; "session_limit_confetti_title" = "Confete do limite de sessão"; @@ -729,7 +726,7 @@ "Resets now" = "Renova agora"; "reset_tomorrow_format" = "amanhã, %@"; "Lasts until reset" = "Dura até a renovação"; -"Try 1.5x!" = "Try 1.5x!"; +"1.5× headroom" = "folga de 1,5×"; "Updated %@" = "Atualizado %@"; "Updated relative %@" = "Atualizado %@"; "Updated absolute %@" = "Atualizado %@"; diff --git a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings index 655a041689..0fa4b4f9f9 100644 --- a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings @@ -549,9 +549,6 @@ "no_writable_bin_dirs" = "Inga skrivbara bin-kataloger hittades."; "show_debug_settings_title" = "Visa felsökningsinställningar"; "show_debug_settings_subtitle" = "Visa felsökningsverktyg på fliken Felsök."; -"historical_pace_weeks_title" = "Historical pace weeks"; -"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; -"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Överraska mig"; "surprise_me_subtitle" = "Kontrollera om du vill att agenterna ska få leka lite där uppe."; "session_limit_confetti_title" = "Konfetti för sessionsgräns"; @@ -731,7 +728,7 @@ "Resets now" = "Återställs nu"; "reset_tomorrow_format" = "imorgon %@"; "Lasts until reset" = "Räcker till återställning"; -"Try 1.5x!" = "Try 1.5x!"; +"1.5× headroom" = "1,5× marginal"; "Updated %@" = "Uppdaterad %@"; "Updated relative %@" = "Uppdaterad %@"; "Updated absolute %@" = "Uppdaterad %@"; diff --git a/Sources/CodexBar/Resources/th.lproj/Localizable.strings b/Sources/CodexBar/Resources/th.lproj/Localizable.strings index 2147269f90..0919875fcd 100644 --- a/Sources/CodexBar/Resources/th.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/th.lproj/Localizable.strings @@ -553,9 +553,6 @@ "no_writable_bin_dirs" = "ไม่พบ bin dirs ที่เขียนได้"; "show_debug_settings_title" = "แสดงการตั้งค่าการดีบัก"; "show_debug_settings_subtitle" = "แสดงเครื่องมือการแก้ไขปัญหาในแท็บ แก้ไขข้อบกพร่อง"; -"historical_pace_weeks_title" = "Historical pace weeks"; -"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; -"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "ทําให้ฉันประหลาดใจ"; "surprise_me_subtitle" = "ตรวจสอบว่าคุณชอบให้ตัวแทนของคุณสนุกสนานที่นั่นหรือไม่"; "session_limit_confetti_title" = "คอนเฟตตีขีดจำกัดเซสชัน"; @@ -735,7 +732,7 @@ "Resets now" = "รีเซ็ตเดี๋ยวนี้"; "reset_tomorrow_format" = "พรุ่งนี้ %@"; "Lasts until reset" = "คงอยู่จนกว่าจะรีเซ็ต"; -"Try 1.5x!" = "Try 1.5x!"; +"1.5× headroom" = "เผื่อ 1.5×"; "Updated %@" = "อัพเดท %@"; "Updated relative %@" = "อัพเดท %@"; "Updated absolute %@" = "อัพเดท %@"; diff --git a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings index b94533cf0b..501dd51e60 100644 --- a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings @@ -553,9 +553,6 @@ "no_writable_bin_dirs" = "Yazılabilir bin dizini bulunamadı."; "show_debug_settings_title" = "Hata Ayıklama Ayarlarını Göster"; "show_debug_settings_subtitle" = "Sorun giderme araçlarını Hata Ayıklama sekmesinde göster."; -"historical_pace_weeks_title" = "Historical pace weeks"; -"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; -"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Beni şaşırt"; "surprise_me_subtitle" = "Ajanlarınızın yukarıda biraz eğlenmesini istiyorsanız işaretleyin."; "session_limit_confetti_title" = "Oturum sınırı konfeti"; @@ -733,7 +730,7 @@ "Resets now" = "Şimdi sıfırlanır"; "reset_tomorrow_format" = "yarın, %@"; "Lasts until reset" = "Sıfırlamaya kadar sürer"; -"Try 1.5x!" = "Try 1.5x!"; +"1.5× headroom" = "1,5× pay"; "Updated %@" = "Güncellendi: %@"; "Updated relative %@" = "Güncellendi: %@"; "Updated absolute %@" = "Güncellendi: %@"; diff --git a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings index 014fa8f8da..0fcdc91600 100644 --- a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings @@ -550,9 +550,6 @@ "no_writable_bin_dirs" = "Не знайдено записуваних каталогів кошика."; "show_debug_settings_title" = "Показати налаштування налагодження"; "show_debug_settings_subtitle" = "Розкрийте інструменти усунення несправностей на вкладці Debug."; -"historical_pace_weeks_title" = "Historical pace weeks"; -"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; -"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Здивуйте мене"; "surprise_me_subtitle" = "Перевірте, чи подобається вам, що ваші агенти розважаються там."; "session_limit_confetti_title" = "Конфеті для ліміту сесії"; @@ -732,7 +729,7 @@ "Resets now" = "Скидає зараз"; "reset_tomorrow_format" = "завтра, %@"; "Lasts until reset" = "Триває до скидання"; -"Try 1.5x!" = "Try 1.5x!"; +"1.5× headroom" = "запас 1,5×"; "Updated %@" = "Оновлено %@"; "Updated relative %@" = "Оновлено %@"; "Updated absolute %@" = "Оновлено %@"; diff --git a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings index 52aeb6a272..58e9e2ebc2 100644 --- a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings @@ -546,9 +546,6 @@ "no_writable_bin_dirs" = "Không tìm thấy thư mục bin có thể ghi."; "show_debug_settings_title" = "Hiển thị gỡ lỗi Cài đặt"; "show_debug_settings_subtitle" = "Hiển thị các công cụ khắc phục sự cố trong tab Gỡ lỗi."; -"historical_pace_weeks_title" = "Historical pace weeks"; -"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; -"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "Làm tôi ngạc nhiên"; "surprise_me_subtitle" = "Kiểm tra xem bạn có thích các đại lý của mình vui vẻ ở đó không."; "session_limit_confetti_title" = "Pháo giấy giới hạn phiên"; @@ -728,7 +725,7 @@ "Resets now" = "Đặt lại ngay"; "reset_tomorrow_format" = "ngày mai, %@"; "Lasts until reset" = "Kéo dài cho đến Đặt lại"; -"Try 1.5x!" = "Try 1.5x!"; +"1.5× headroom" = "dư địa 1,5×"; "Updated %@" = "Đã cập nhật %@"; "Updated relative %@" = "Đã cập nhật %@"; "Updated absolute %@" = "Đã cập nhật %@"; diff --git a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings index 00e2c7b591..8d2431dcc8 100644 --- a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings @@ -535,9 +535,6 @@ "no_writable_bin_dirs" = "未找到可写的 bin 目录。"; "show_debug_settings_title" = "显示调试设置"; "show_debug_settings_subtitle" = "在“调试”标签中显示故障排除工具。"; -"historical_pace_weeks_title" = "Historical pace weeks"; -"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; -"historical_pace_weeks_value" = "%d weeks"; "surprise_me_title" = "给我惊喜"; "surprise_me_subtitle" = "看看你是否喜欢你的智能体在上面找点乐子。"; "session_limit_confetti_title" = "会话限制彩带"; @@ -706,7 +703,7 @@ "Resets now" = "立即重置"; "reset_tomorrow_format" = "明天 %@"; "Lasts until reset" = "持续到重置"; -"Try 1.5x!" = "Try 1.5x!"; +"1.5× headroom" = "1.5 倍余量"; "Updated %@" = "更新于 %@"; "Updated relative %@" = "%@已更新"; "Updated absolute %@" = "更新于 %@"; diff --git a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings index 2bc1f2baa1..5cbea09091 100644 --- a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings @@ -549,9 +549,7 @@ "no_writable_bin_dirs" = "找不到可寫的 bin 目錄。"; "show_debug_settings_title" = "顯示除錯設定"; "show_debug_settings_subtitle" = "在「除錯」標籤中顯示疑難排解工具。"; -"historical_pace_weeks_title" = "Historical pace weeks"; -"historical_pace_weeks_subtitle" = "Minimum complete Codex weeks before historical pace replaces linear pace."; -"historical_pace_weeks_value" = "%d weeks"; +"1.5× headroom" = "1.5 倍餘裕"; "surprise_me_title" = "給我驚喜"; "surprise_me_subtitle" = "讓選單列上的 Agent 多一點變化。"; "session_limit_confetti_title" = "工作階段限制彩帶"; diff --git a/Sources/CodexBar/SettingsStore+Defaults.swift b/Sources/CodexBar/SettingsStore+Defaults.swift index ce967d5d79..666da74e65 100644 --- a/Sources/CodexBar/SettingsStore+Defaults.swift +++ b/Sources/CodexBar/SettingsStore+Defaults.swift @@ -306,15 +306,6 @@ extension SettingsStore { } } - var historicalPaceMinimumWeeks: Int { - get { self.defaultsState.historicalPaceMinimumWeeks } - set { - let clamped = max(2, min(8, newValue)) - self.defaultsState.historicalPaceMinimumWeeks = clamped - self.userDefaults.set(clamped, forKey: "historicalPaceMinimumWeeks") - } - } - var menuBarMetricPreferencesRaw: [String: String] { get { self.defaultsState.menuBarMetricPreferencesRaw } set { diff --git a/Sources/CodexBar/SettingsStore+MenuObservation.swift b/Sources/CodexBar/SettingsStore+MenuObservation.swift index 1b5f9a8ae7..a22f411e31 100644 --- a/Sources/CodexBar/SettingsStore+MenuObservation.swift +++ b/Sources/CodexBar/SettingsStore+MenuObservation.swift @@ -30,7 +30,6 @@ extension SettingsStore { _ = self.menuBarDisplayMode _ = self.kiroMenuBarDisplayMode _ = self.historicalTrackingEnabled - _ = self.historicalPaceMinimumWeeks _ = self.multiAccountMenuLayout _ = self.menuBarMetricPreferencesRaw _ = self.copilotIconSecondaryWindowIDRaw diff --git a/Sources/CodexBar/SettingsStore.swift b/Sources/CodexBar/SettingsStore.swift index f65d8df836..a60f44f409 100644 --- a/Sources/CodexBar/SettingsStore.swift +++ b/Sources/CodexBar/SettingsStore.swift @@ -402,8 +402,6 @@ extension SettingsStore { let kiroMenuBarDisplayModeRaw = userDefaults.string(forKey: "kiroMenuBarDisplayMode") ?? KiroMenuBarDisplayMode.automatic.rawValue let historicalTrackingEnabled = userDefaults.object(forKey: "historicalTrackingEnabled") as? Bool ?? false - let rawHistoricalPaceMinimumWeeks = userDefaults.object(forKey: "historicalPaceMinimumWeeks") as? Int ?? 3 - let historicalPaceMinimumWeeks = max(2, min(8, rawHistoricalPaceMinimumWeeks)) let multiAccountMenuLayoutRaw = Self.loadMultiAccountMenuLayoutRaw(userDefaults: userDefaults) let resolvedPreferences = Self.loadMenuBarMetricPreferences(userDefaults: userDefaults) let copilotBudgetExtrasEnabled = userDefaults.object(forKey: "copilotBudgetExtrasEnabled") as? Bool ?? false @@ -483,7 +481,6 @@ extension SettingsStore { menuBarDisplayModeRaw: menuBarDisplayModeRaw, kiroMenuBarDisplayModeRaw: kiroMenuBarDisplayModeRaw, historicalTrackingEnabled: historicalTrackingEnabled, - historicalPaceMinimumWeeks: historicalPaceMinimumWeeks, multiAccountMenuLayoutRaw: multiAccountMenuLayoutRaw, menuBarMetricPreferencesRaw: resolvedPreferences, copilotBudgetExtrasEnabled: copilotBudgetExtrasEnabled, diff --git a/Sources/CodexBar/SettingsStoreState.swift b/Sources/CodexBar/SettingsStoreState.swift index 2a288220aa..e72d6d794a 100644 --- a/Sources/CodexBar/SettingsStoreState.swift +++ b/Sources/CodexBar/SettingsStoreState.swift @@ -30,7 +30,6 @@ struct SettingsDefaultsState { var menuBarDisplayModeRaw: String? var kiroMenuBarDisplayModeRaw: String? var historicalTrackingEnabled: Bool - var historicalPaceMinimumWeeks: Int var multiAccountMenuLayoutRaw: String var menuBarMetricPreferencesRaw: [String: String] var copilotBudgetExtrasEnabled: Bool diff --git a/Sources/CodexBar/UsagePaceText.swift b/Sources/CodexBar/UsagePaceText.swift index 444739c253..23dd00fc4f 100644 --- a/Sources/CodexBar/UsagePaceText.swift +++ b/Sources/CodexBar/UsagePaceText.swift @@ -14,18 +14,18 @@ enum UsagePaceText { case weekly } - static func weeklySummary(pace: UsagePace, now: Date = .init()) -> String { - let detail = self.weeklyDetail(pace: pace, now: now) + static func weeklySummary(provider: UsageProvider, pace: UsagePace, now: Date = .init()) -> String { + let detail = self.weeklyDetail(provider: provider, pace: pace, now: now) if let rightLabel = detail.rightLabel { return L("Pace: %@ · %@", detail.leftLabel, rightLabel) } return L("Pace: %@", detail.leftLabel) } - static func weeklyDetail(pace: UsagePace, now: Date = .init()) -> WeeklyDetail { + static func weeklyDetail(provider: UsageProvider, pace: UsagePace, now: Date = .init()) -> WeeklyDetail { WeeklyDetail( leftLabel: self.detailLeftLabel(for: pace), - rightLabel: self.detailRightLabel(for: pace, context: .weekly, now: now), + rightLabel: self.detailRightLabel(for: pace, provider: provider, context: .weekly, now: now), expectedUsedPercent: pace.expectedUsedPercent, stage: pace.stage) } @@ -45,10 +45,15 @@ enum UsagePaceText { } } - private static func detailRightLabel(for pace: UsagePace, context: DetailContext, now: Date) -> String? { + private static func detailRightLabel( + for pace: UsagePace, + provider: UsageProvider, + context: DetailContext, + now: Date) -> String? + { let etaLabel: String? if pace.willLastToReset { - etaLabel = self.combinedLastsLabel(for: pace) + etaLabel = self.combinedLastsLabel(for: pace, provider: provider) } else if let etaSeconds = pace.etaSeconds { let etaText = Self.durationText(seconds: etaSeconds, now: now) if context == .session { @@ -72,7 +77,8 @@ enum UsagePaceText { return riskLabel } - private static func combinedLastsLabel(for pace: UsagePace) -> String { + private static func combinedLastsLabel(for pace: UsagePace, provider: UsageProvider) -> String { + guard provider == .codex else { return L("Lasts until reset") } guard let speedLabel = self.speedHintLabel(for: pace) else { return L("Lasts until reset") } @@ -84,7 +90,7 @@ enum UsagePaceText { let multiplier = pace.speedMultiplierToReset, multiplier >= 1.5 else { return nil } - return L("Try 1.5x!") + return L("1.5× headroom") } private static func durationText(seconds: TimeInterval, now: Date) -> String { @@ -114,7 +120,7 @@ enum UsagePaceText { guard let pace = sessionPace(provider: provider, window: window, now: now) else { return nil } return WeeklyDetail( leftLabel: Self.detailLeftLabel(for: pace), - rightLabel: Self.detailRightLabel(for: pace, context: .session, now: now), + rightLabel: Self.detailRightLabel(for: pace, provider: provider, context: .session, now: now), expectedUsedPercent: pace.expectedUsedPercent, stage: pace.stage) } diff --git a/Sources/CodexBar/UsageStore+HistoricalPace.swift b/Sources/CodexBar/UsageStore+HistoricalPace.swift index ec9d7a3fda..cda21506b2 100644 --- a/Sources/CodexBar/UsageStore+HistoricalPace.swift +++ b/Sources/CodexBar/UsageStore+HistoricalPace.swift @@ -19,8 +19,7 @@ extension UsageStore { let historical = CodexHistoricalPaceEvaluator.evaluate( window: window, now: now, - dataset: self.codexHistoricalDataset, - minimumCompleteWeeks: self.settings.historicalPaceMinimumWeeks) + dataset: self.codexHistoricalDataset) { resolved = historical } else { diff --git a/Sources/CodexBarCLI/CLIRenderer.swift b/Sources/CodexBarCLI/CLIRenderer.swift index 5434bd454f..4eda4aa4a2 100644 --- a/Sources/CodexBarCLI/CLIRenderer.swift +++ b/Sources/CodexBarCLI/CLIRenderer.swift @@ -555,12 +555,17 @@ enum CLIRenderer { return pace } - private static func paceSummary(for pace: UsagePace, kind: PaceKind, now: Date) -> String { + private static func paceSummary( + provider: UsageProvider, + for pace: UsagePace, + kind: PaceKind, + now: Date) -> String + { let expected = Int(pace.expectedUsedPercent.rounded()) var parts: [String] = [] parts.append(Self.paceLeftLabel(for: pace)) parts.append("Expected \(expected)% used") - if let rightLabel = Self.paceRightLabel(for: pace, kind: kind, now: now) { + if let rightLabel = Self.paceRightLabel(provider: provider, for: pace, kind: kind, now: now) { parts.append(rightLabel) } return parts.joined(separator: " | ") @@ -581,7 +586,7 @@ enum CLIRenderer { weeklyWorkDays: weeklyWorkDays, now: now) else { return nil } let label = self.label("Pace", useColor: useColor) - return "\(label): \(self.paceSummary(for: pace, kind: kind, now: now))" + return "\(label): \(self.paceSummary(provider: provider, for: pace, kind: kind, now: now))" } private static func pacePayload( @@ -604,7 +609,7 @@ enum CLIRenderer { willLastToReset: pace.willLastToReset, etaSeconds: pace.etaSeconds.map { $0.rounded() }, runOutProbability: pace.runOutProbability, - summary: self.paceSummary(for: pace, kind: kind, now: now)) + summary: self.paceSummary(provider: provider, for: pace, kind: kind, now: now)) } private static func stageString(_ stage: UsagePace.Stage) -> String { @@ -631,8 +636,13 @@ enum CLIRenderer { } } - private static func paceRightLabel(for pace: UsagePace, kind: PaceKind, now: Date) -> String? { - if pace.willLastToReset { return self.combinedLastsLabel(for: pace) } + private static func paceRightLabel( + provider: UsageProvider, + for pace: UsagePace, + kind: PaceKind, + now: Date) -> String? + { + if pace.willLastToReset { return self.combinedLastsLabel(for: pace, provider: provider) } guard let etaSeconds = pace.etaSeconds else { return nil } let etaText = Self.paceDurationText(seconds: etaSeconds, now: now) switch kind { @@ -643,7 +653,8 @@ enum CLIRenderer { } } - private static func combinedLastsLabel(for pace: UsagePace) -> String { + private static func combinedLastsLabel(for pace: UsagePace, provider: UsageProvider) -> String { + guard provider == .codex else { return "Lasts until reset" } guard let speedLabel = speedHintLabel(for: pace) else { return "Lasts until reset" } @@ -655,7 +666,7 @@ enum CLIRenderer { let multiplier = pace.speedMultiplierToReset, multiplier >= 1.5 else { return nil } - return "Try 1.5x!" + return "1.5× headroom" } private static func paceDurationText(seconds: TimeInterval, now: Date) -> String { diff --git a/Tests/CodexBarTests/CLISnapshotTests.swift b/Tests/CodexBarTests/CLISnapshotTests.swift index 27b205887d..cf5eee9d37 100644 --- a/Tests/CodexBarTests/CLISnapshotTests.swift +++ b/Tests/CodexBarTests/CLISnapshotTests.swift @@ -526,7 +526,7 @@ struct CLISnapshotTests { #expect(output.contains("Weekly: 77% left")) #expect(output.contains("Pace: 6% in reserve | Expected 29% used | Lasts until reset")) - #expect(!output.contains("Try 1.5x!")) + #expect(!output.contains("1.5× headroom")) } @Test @@ -582,7 +582,7 @@ struct CLISnapshotTests { #expect(output.contains("Session: 80% left")) // 2h remaining of a 5h window => 3h elapsed => 60% expected; even rate easily lasts to reset. - #expect(output.contains("Pace: 40% in reserve | Expected 60% used | Lasts until reset | Try 1.5x!")) + #expect(output.contains("Pace: 40% in reserve | Expected 60% used | Lasts until reset | 1.5× headroom")) } @Test @@ -610,7 +610,8 @@ struct CLISnapshotTests { now: now) // windowMinutes is nil, so the 5-hour (300 minute) session default must drive the pace. - #expect(output.contains("Pace: 40% in reserve | Expected 60% used | Lasts until reset | Try 1.5x!")) + #expect(output.contains("Pace: 40% in reserve | Expected 60% used | Lasts until reset")) + #expect(!output.contains("1.5× headroom")) } @Test diff --git a/Tests/CodexBarTests/HistoricalUsagePaceTests.swift b/Tests/CodexBarTests/HistoricalUsagePaceTests.swift index 103a6cd83b..e294baa6e7 100644 --- a/Tests/CodexBarTests/HistoricalUsagePaceTests.swift +++ b/Tests/CodexBarTests/HistoricalUsagePaceTests.swift @@ -785,7 +785,7 @@ struct HistoricalUsagePaceTests { #expect(try #require(pace.runOutProbability) > 0) #expect(pace.deltaPercent > 0) - let detail = UsagePaceText.weeklyDetail(pace: pace, now: now) + let detail = UsagePaceText.weeklyDetail(provider: .codex, pace: pace, now: now) #expect(detail.leftLabel == "5% in deficit") #expect(detail.rightLabel?.contains("Lasts until reset") == false) } @@ -816,7 +816,7 @@ struct HistoricalUsagePaceTests { #expect(pace.etaSeconds == 0) #expect(pace.runOutProbability == 1) - let detail = UsagePaceText.weeklyDetail(pace: pace, now: now) + let detail = UsagePaceText.weeklyDetail(provider: .codex, pace: pace, now: now) #expect(detail.rightLabel == "Runs out now · ≈ 100% run-out risk") } } diff --git a/Tests/CodexBarTests/SettingsStoreCoverageTests.swift b/Tests/CodexBarTests/SettingsStoreCoverageTests.swift index b56517470d..22e0724a2f 100644 --- a/Tests/CodexBarTests/SettingsStoreCoverageTests.swift +++ b/Tests/CodexBarTests/SettingsStoreCoverageTests.swift @@ -69,13 +69,6 @@ struct SettingsStoreCoverageTests { #expect(settings.historicalTrackingEnabled == false) settings.historicalTrackingEnabled = true #expect(settings.historicalTrackingEnabled == true) - #expect(settings.historicalPaceMinimumWeeks == 3) - settings.historicalPaceMinimumWeeks = 1 - #expect(settings.historicalPaceMinimumWeeks == 2) - settings.historicalPaceMinimumWeeks = 9 - #expect(settings.historicalPaceMinimumWeeks == 8) - settings.historicalPaceMinimumWeeks = 2 - #expect(settings.historicalPaceMinimumWeeks == 2) settings.resetTimesShowAbsolute = true #expect(settings.resetTimeDisplayStyle == .absolute) diff --git a/Tests/CodexBarTests/UsagePaceTextTests.swift b/Tests/CodexBarTests/UsagePaceTextTests.swift index 5ad5cbce75..8e3bc88b2c 100644 --- a/Tests/CodexBarTests/UsagePaceTextTests.swift +++ b/Tests/CodexBarTests/UsagePaceTextTests.swift @@ -15,7 +15,7 @@ struct UsagePaceTextTests { "Projected empty in %@", "Runs out now", "Runs out in %@", - "Try 1.5x!", + "1.5× headroom", "≈ %d%% run-out risk", "%@ · %@", ] @@ -30,7 +30,7 @@ struct UsagePaceTextTests { resetDescription: nil) let pace = try #require(UsagePace.weekly(window: window, now: now)) - let detail = UsagePaceText.weeklyDetail(pace: pace, now: now) + let detail = UsagePaceText.weeklyDetail(provider: .codex, pace: pace, now: now) #expect(detail.leftLabel == "7% in deficit") #expect(detail.rightLabel == "Runs out in 3d") @@ -47,7 +47,7 @@ struct UsagePaceTextTests { etaSeconds: nil, willLastToReset: true) - let detail = UsagePaceText.weeklyDetail(pace: pace, now: now) + let detail = UsagePaceText.weeklyDetail(provider: .codex, pace: pace, now: now) #expect(detail.leftLabel == "On pace") } @@ -62,10 +62,10 @@ struct UsagePaceTextTests { resetDescription: nil) let pace = try #require(UsagePace.weekly(window: window, now: now)) - let detail = UsagePaceText.weeklyDetail(pace: pace, now: now) + let detail = UsagePaceText.weeklyDetail(provider: .codex, pace: pace, now: now) #expect(detail.leftLabel == "33% in reserve") - #expect(detail.rightLabel == "Lasts until reset · Try 1.5x!") + #expect(detail.rightLabel == "Lasts until reset · 1.5× headroom") } @Test @@ -78,7 +78,7 @@ struct UsagePaceTextTests { resetDescription: nil) let pace = try #require(UsagePace.weekly(window: window, now: now)) - let summary = UsagePaceText.weeklySummary(pace: pace, now: now) + let summary = UsagePaceText.weeklySummary(provider: .codex, pace: pace, now: now) #expect(summary == "Pace: 7% in deficit · Runs out in 3d") } @@ -93,10 +93,25 @@ struct UsagePaceTextTests { resetDescription: nil) let pace = try #require(UsagePace.weekly(window: window, now: now)) - let detail = UsagePaceText.weeklyDetail(pace: pace, now: now) + let detail = UsagePaceText.weeklyDetail(provider: .codex, pace: pace, now: now) #expect(detail.leftLabel == "37% in reserve") - #expect(detail.rightLabel == "Lasts until reset · Try 1.5x!") + #expect(detail.rightLabel == "Lasts until reset · 1.5× headroom") + } + + @Test + func `weekly pace detail limits headroom hint to Codex`() throws { + let now = Date(timeIntervalSince1970: 0) + let window = RateWindow( + usedPercent: 20, + windowMinutes: 10080, + resetsAt: now.addingTimeInterval(3 * 24 * 3600), + resetDescription: nil) + let pace = try #require(UsagePace.weekly(window: window, now: now)) + + let detail = UsagePaceText.weeklyDetail(provider: .claude, pace: pace, now: now) + + #expect(detail.rightLabel == "Lasts until reset") } @Test @@ -109,10 +124,10 @@ struct UsagePaceTextTests { resetDescription: nil) let pace = try #require(UsagePace.weekly(window: window, now: now)) - let detail = UsagePaceText.weeklyDetail(pace: pace, now: now) + let detail = UsagePaceText.weeklyDetail(provider: .codex, pace: pace, now: now) #expect(detail.leftLabel == "20% in reserve") - #expect(detail.rightLabel == "Lasts until reset · Try 1.5x!") + #expect(detail.rightLabel == "Lasts until reset · 1.5× headroom") } @Test @@ -125,7 +140,7 @@ struct UsagePaceTextTests { resetDescription: nil) let pace = try #require(UsagePace.weekly(window: window, now: now, workDays: nil)) - let detail = UsagePaceText.weeklyDetail(pace: pace, now: now) + let detail = UsagePaceText.weeklyDetail(provider: .codex, pace: pace, now: now) #expect(detail.leftLabel == "28% in deficit") #expect(detail.rightLabel == "Runs out in 13h 47m") @@ -144,7 +159,7 @@ struct UsagePaceTextTests { willLastToReset: false, runOutProbability: 0.683) - let detail = UsagePaceText.weeklyDetail(pace: pace, now: now) + let detail = UsagePaceText.weeklyDetail(provider: .codex, pace: pace, now: now) #expect(detail.rightLabel == "Runs out in 2d · ≈ 70% run-out risk") } @@ -161,7 +176,7 @@ struct UsagePaceTextTests { willLastToReset: true, runOutProbability: 0.45) - let detail = UsagePaceText.weeklyDetail(pace: pace, now: now) + let detail = UsagePaceText.weeklyDetail(provider: .codex, pace: pace, now: now) #expect(detail.leftLabel == "9% in reserve") #expect(detail.rightLabel == "≈ 45% run-out risk") @@ -180,9 +195,9 @@ struct UsagePaceTextTests { runOutProbability: 0.02, speedMultiplierToReset: 4) - let detail = UsagePaceText.weeklyDetail(pace: pace, now: now) + let detail = UsagePaceText.weeklyDetail(provider: .codex, pace: pace, now: now) - #expect(detail.rightLabel == "Lasts until reset · Try 1.5x! · ≈ 0% run-out risk") + #expect(detail.rightLabel == "Lasts until reset · 1.5× headroom · ≈ 0% run-out risk") } @Test @@ -197,7 +212,7 @@ struct UsagePaceTextTests { willLastToReset: true, runOutProbability: 0.03) - let detail = UsagePaceText.weeklyDetail(pace: pace, now: now) + let detail = UsagePaceText.weeklyDetail(provider: .codex, pace: pace, now: now) #expect(detail.rightLabel == "≈ 5% run-out risk") } @@ -224,7 +239,7 @@ struct UsagePaceTextTests { } @Test - func `session pace detail reports lasts until reset`() { + func `Claude session pace does not show Codex headroom`() { let now = Date(timeIntervalSince1970: 0) // 300-minute window, 2h remaining => 3h elapsed // expected = 60%, actual = 10% => far behind (in reserve) @@ -238,7 +253,21 @@ struct UsagePaceTextTests { #expect(detail != nil) #expect(detail?.leftLabel == "50% in reserve") - #expect(detail?.rightLabel == "Lasts until reset · Try 1.5x!") + #expect(detail?.rightLabel == "Lasts until reset") + } + + @Test + func `Codex session pace shows conservative headroom`() { + let now = Date(timeIntervalSince1970: 0) + let window = RateWindow( + usedPercent: 10, + windowMinutes: 300, + resetsAt: now.addingTimeInterval(2 * 3600), + resetDescription: nil) + + let detail = UsagePaceText.sessionDetail(provider: .codex, window: window, now: now) + + #expect(detail?.rightLabel == "Lasts until reset · 1.5× headroom") } @Test