diff --git a/Sources/CodexBar/MenuCardView+ModelHelpers.swift b/Sources/CodexBar/MenuCardView+ModelHelpers.swift index 94d7c0743c..dfa70bc44f 100644 --- a/Sources/CodexBar/MenuCardView+ModelHelpers.swift +++ b/Sources/CodexBar/MenuCardView+ModelHelpers.swift @@ -30,6 +30,23 @@ extension UsageMenuCardView.Model { let rightLabel: String? let pacePercent: Double? let paceOnTop: Bool + /// True only for text produced by a pace calculation. Defaults to false + /// so provider-owned text sharing the detail slot is preserved. + let isPaceDerived: Bool + + init( + leftLabel: String, + rightLabel: String?, + pacePercent: Double?, + paceOnTop: Bool, + isPaceDerived: Bool = false) + { + self.leftLabel = leftLabel + self.rightLabel = rightLabel + self.pacePercent = pacePercent + self.paceOnTop = paceOnTop + self.isPaceDerived = isPaceDerived + } } struct PrimaryMetricPresentation { @@ -40,6 +57,7 @@ extension UsageMenuCardView.Model { var detailRight: String? var pacePercent: Double? var paceOnTop = true + var detailIsPaceDerived = false } static func applyPrimaryQuotaPresentation( @@ -187,6 +205,7 @@ extension UsageMenuCardView.Model { presentation.detailRight = paceDetail.rightLabel presentation.pacePercent = paceDetail.pacePercent presentation.paceOnTop = paceDetail.paceOnTop + presentation.detailIsPaceDerived = paceDetail.isPaceDerived } private static func nonEmptyResetDescription(_ window: RateWindow) -> String? { @@ -215,6 +234,36 @@ extension UsageMenuCardView.Model { return PersonalInfoRedactor.redactEmails(in: "Team\(detail[separator.lowerBound...])", isEnabled: true) } + /// Clears the pace stripe and the forecast text when the user hides pace. + /// Copies every `Metric` field so unrelated decorations (quota and workday + /// ticks) survive; dropping one here would silently disable them. + static func paceGatedMetrics(_ metrics: [Metric], paceVisible: Bool) -> [Metric] { + guard !paceVisible else { return metrics } + return metrics.map { metric in + // The detail slots are shared: providers such as Kiro, Copilot, and + // ZenMux put their own credit and reset text there. Clear them only + // when they carry a pace forecast. + Metric( + id: metric.id, + title: metric.title, + percent: metric.percent, + percentStyle: metric.percentStyle, + statusText: metric.statusText, + resetText: metric.resetText, + detailText: metric.detailText, + detailLeftText: metric.detailIsPaceDerived ? nil : metric.detailLeftText, + detailRightText: metric.detailIsPaceDerived ? nil : metric.detailRightText, + pacePercent: nil, + detailIsPaceDerived: metric.detailIsPaceDerived, + paceOnTop: metric.paceOnTop, + warningMarkerPercents: metric.warningMarkerPercents, + workdayMarkerPercents: metric.workdayMarkerPercents, + workdayTickAppearance: metric.workdayTickAppearance, + cardStyle: metric.cardStyle, + sessionEquivalentDetail: nil) + } + } + static func redactedMetrics( _ metrics: [Metric], provider: UsageProvider, @@ -236,6 +285,7 @@ extension UsageMenuCardView.Model { detailLeftText: PersonalInfoRedactor.redactEmails(in: metric.detailLeftText, isEnabled: true), detailRightText: PersonalInfoRedactor.redactEmails(in: metric.detailRightText, isEnabled: true), pacePercent: metric.pacePercent, + detailIsPaceDerived: metric.detailIsPaceDerived, paceOnTop: metric.paceOnTop, warningMarkerPercents: metric.warningMarkerPercents, workdayMarkerPercents: metric.workdayMarkerPercents, @@ -706,7 +756,8 @@ extension UsageMenuCardView.Model { leftLabel: detail.leftLabel, rightLabel: detail.rightLabel, pacePercent: pacePercent, - paceOnTop: paceOnTop) + paceOnTop: paceOnTop, + isPaceDerived: true) } static func weeklyPaceDetail( @@ -735,7 +786,8 @@ extension UsageMenuCardView.Model { leftLabel: detail.leftLabel, rightLabel: detail.rightLabel, pacePercent: pacePercent, - paceOnTop: paceOnTop) + paceOnTop: paceOnTop, + isPaceDerived: true) } static func standardWeeklyPace(input: Input, window: RateWindow) -> UsagePace? { @@ -897,6 +949,7 @@ extension UsageMenuCardView.Model { detailLeftText: usageKnown ? paceDetail?.leftLabel : nil, detailRightText: usageKnown ? paceDetail?.rightLabel : nil, pacePercent: usageKnown ? paceDetail?.pacePercent : nil, + detailIsPaceDerived: paceDetail?.isPaceDerived ?? false, paceOnTop: paceDetail?.paceOnTop ?? true, sessionEquivalentDetail: usageKnown ? Self.sessionEquivalentDetail( @@ -1061,6 +1114,7 @@ extension UsageMenuCardView.Model { detailLeftText: paceDetail?.leftLabel, detailRightText: paceDetail?.rightLabel, pacePercent: paceDetail?.pacePercent, + detailIsPaceDerived: paceDetail?.isPaceDerived ?? false, paceOnTop: paceDetail?.paceOnTop ?? true) } @@ -1093,7 +1147,12 @@ extension UsageMenuCardView.Model { } else { String(format: L("Full in ~%.0f regens"), ceil(ticksToFull)) } - return (resetText, PaceDetail(leftLabel: left, rightLabel: right, pacePercent: nil, paceOnTop: true)) + return (resetText, PaceDetail( + leftLabel: left, + rightLabel: right, + pacePercent: nil, + paceOnTop: true, + isPaceDerived: true)) } static func syntheticRollingRegenDetail( @@ -1124,6 +1183,11 @@ extension UsageMenuCardView.Model { String(format: L("Full in ~%.0f regens"), ceil(ticksToFull)) } - return (resetText, PaceDetail(leftLabel: left, rightLabel: right, pacePercent: nil, paceOnTop: true)) + return (resetText, PaceDetail( + leftLabel: left, + rightLabel: right, + pacePercent: nil, + paceOnTop: true, + isPaceDerived: true)) } } diff --git a/Sources/CodexBar/MenuCardView+ModelInput.swift b/Sources/CodexBar/MenuCardView+ModelInput.swift index 12210aa1b5..c8edf11c13 100644 --- a/Sources/CodexBar/MenuCardView+ModelInput.swift +++ b/Sources/CodexBar/MenuCardView+ModelInput.swift @@ -43,6 +43,7 @@ extension UsageMenuCardView.Model { let quotaWarningThresholds: [QuotaWarningWindow: [Int]] let workDaysPerWeek: Int? let workdayTickAppearance: WorkdayTickAppearance + let paceVisible: Bool let usesLiveSubtitle: Bool let preferredCurrencyCode: String let now: Date @@ -86,6 +87,7 @@ extension UsageMenuCardView.Model { quotaWarningThresholds: [QuotaWarningWindow: [Int]] = [:], workDaysPerWeek: Int? = nil, workdayTickAppearance: WorkdayTickAppearance = .subtle, + paceVisible: Bool = true, usesLiveSubtitle: Bool = false, preferredCurrencyCode: String = "auto", now: Date) @@ -128,6 +130,7 @@ extension UsageMenuCardView.Model { self.quotaWarningThresholds = quotaWarningThresholds self.workDaysPerWeek = workDaysPerWeek self.workdayTickAppearance = workdayTickAppearance + self.paceVisible = paceVisible self.usesLiveSubtitle = usesLiveSubtitle self.preferredCurrencyCode = preferredCurrencyCode self.now = now diff --git a/Sources/CodexBar/MenuCardView+SessionEquivalent.swift b/Sources/CodexBar/MenuCardView+SessionEquivalent.swift index 6d19bc5639..12fc4be82b 100644 --- a/Sources/CodexBar/MenuCardView+SessionEquivalent.swift +++ b/Sources/CodexBar/MenuCardView+SessionEquivalent.swift @@ -69,6 +69,7 @@ extension UsageMenuCardView.Model { detailLeftText: paceDetail?.leftLabel, detailRightText: paceDetail?.rightLabel, pacePercent: paceDetail?.pacePercent, + detailIsPaceDerived: paceDetail?.isPaceDerived ?? false, paceOnTop: paceDetail?.paceOnTop ?? true, warningMarkerPercents: Self.warningMarkerPercents( thresholds: lane.quotaWarningWindow.flatMap { input.quotaWarningThresholds[$0] }, diff --git a/Sources/CodexBar/MenuCardView.swift b/Sources/CodexBar/MenuCardView.swift index effcb26551..297e611640 100644 --- a/Sources/CodexBar/MenuCardView.swift +++ b/Sources/CodexBar/MenuCardView.swift @@ -41,6 +41,8 @@ struct UsageMenuCardView: View { let detailLeftText: String? let detailRightText: String? let pacePercent: Double? + /// True when detailLeftText/detailRightText came from a pace forecast. + let detailIsPaceDerived: Bool let paceOnTop: Bool let warningMarkerPercents: [Double] let workdayMarkerPercents: [Double] @@ -59,6 +61,7 @@ struct UsageMenuCardView: View { detailLeftText: String?, detailRightText: String?, pacePercent: Double?, + detailIsPaceDerived: Bool = false, paceOnTop: Bool, warningMarkerPercents: [Double] = [], workdayMarkerPercents: [Double] = [], @@ -76,6 +79,7 @@ struct UsageMenuCardView: View { self.detailLeftText = detailLeftText self.detailRightText = detailRightText self.pacePercent = pacePercent + self.detailIsPaceDerived = detailIsPaceDerived self.paceOnTop = paceOnTop self.warningMarkerPercents = warningMarkerPercents self.workdayMarkerPercents = workdayMarkerPercents @@ -944,7 +948,7 @@ extension UsageMenuCardView.Model { override: input.planOverride, metadata: input.metadata) let metrics = Self.redactedMetrics( - Self.metrics(input: input), + Self.paceGatedMetrics(Self.metrics(input: input), paceVisible: input.paceVisible), provider: input.provider, hidePersonalInfo: input.hidePersonalInfo) let openAIAPIUsage = input.snapshot?.openAIAPIUsage @@ -1327,6 +1331,7 @@ extension UsageMenuCardView.Model { detailLeftText: tertiaryPaceDetail?.leftLabel, detailRightText: tertiaryPaceDetail?.rightLabel, pacePercent: tertiaryPaceDetail?.pacePercent, + detailIsPaceDerived: tertiaryPaceDetail?.isPaceDerived ?? false, paceOnTop: tertiaryPaceDetail?.paceOnTop ?? true, warningMarkerPercents: Self.warningMarkerPercents( thresholds: input.quotaWarningThresholds[.weekly], @@ -1415,6 +1420,7 @@ extension UsageMenuCardView.Model { detailLeftText: presentation.detailLeft, detailRightText: presentation.detailRight, pacePercent: presentation.pacePercent, + detailIsPaceDerived: presentation.detailIsPaceDerived, paceOnTop: presentation.paceOnTop, warningMarkerPercents: Self.warningMarkerPercents( thresholds: input.quotaWarningThresholds[.session], @@ -1543,6 +1549,7 @@ extension UsageMenuCardView.Model { detailLeftText: paceDetail?.leftLabel, detailRightText: paceDetail?.rightLabel, pacePercent: paceDetail?.pacePercent, + detailIsPaceDerived: paceDetail?.isPaceDerived ?? false, paceOnTop: paceDetail?.paceOnTop ?? true, warningMarkerPercents: Self.warningMarkerPercents( thresholds: input.quotaWarningThresholds[.weekly], diff --git a/Sources/CodexBar/MenuDescriptor.swift b/Sources/CodexBar/MenuDescriptor.swift index 8b2761318d..4180d189b3 100644 --- a/Sources/CodexBar/MenuDescriptor.swift +++ b/Sources/CodexBar/MenuDescriptor.swift @@ -271,13 +271,16 @@ struct MenuDescriptor { { entries.append(.text(primaryDetail, .secondary)) } - if presentation.menu.showsPrimaryWeeklyPace, + if settings.paceVisible, + presentation.menu.showsPrimaryWeeklyPace, let pace = store.weeklyPace(provider: provider, window: primary) { let paceSummary = UsagePaceText.weeklySummary(provider: provider, pace: pace) entries.append(.text(paceSummary, .secondary)) } - if let paceSummary = UsagePaceText.sessionSummary(provider: provider, window: primary) { + if settings.paceVisible, + let paceSummary = UsagePaceText.sessionSummary(provider: provider, window: primary) + { entries.append(.text(paceSummary, .secondary)) } } @@ -308,7 +311,9 @@ struct MenuDescriptor { { entries.append(.text(detail, .secondary)) } - if let pace = store.weeklyPace(provider: provider, window: weekly) { + if settings.paceVisible, + let pace = store.weeklyPace(provider: provider, window: weekly) + { let paceSummary = UsagePaceText.weeklySummary(provider: provider, pace: pace) entries.append(.text(paceSummary, .secondary)) } diff --git a/Sources/CodexBar/PreferencesMenuPane.swift b/Sources/CodexBar/PreferencesMenuPane.swift index 242671ee91..478730d404 100644 --- a/Sources/CodexBar/PreferencesMenuPane.swift +++ b/Sources/CodexBar/PreferencesMenuPane.swift @@ -23,6 +23,12 @@ struct MenuPane: View { subtitle: L("show_quota_warning_markers_subtitle")) } + Toggle(isOn: self.$settings.paceVisible) { + SettingsRowLabel( + L("show_pace_title"), + subtitle: L("show_pace_subtitle")) + } + SettingsMenuPicker( selection: self.$settings.weeklyProgressWorkDays, options: MenuSettingsMenuOptions.weeklyProgressWorkDays, diff --git a/Sources/CodexBar/PreferencesProvidersPane.swift b/Sources/CodexBar/PreferencesProvidersPane.swift index 317fc775ba..0d536719ba 100644 --- a/Sources/CodexBar/PreferencesProvidersPane.swift +++ b/Sources/CodexBar/PreferencesProvidersPane.swift @@ -608,6 +608,7 @@ struct ProvidersPane: View { ], workDaysPerWeek: self.settings.weeklyProgressWorkDays, workdayTickAppearance: self.settings.workdayTickAppearance, + paceVisible: self.settings.paceVisible, now: now) return UsageMenuCardView.Model.make(input) } diff --git a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings index 9a83de0576..fe3d7d6976 100644 --- a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings @@ -623,6 +623,8 @@ "display_mode_subtitle" = "اختر ما تريد عرضه في شريط القائمة (Pace يظهر الاستخدام مقابل المتوقع)."; "show_quota_warning_markers_title" = "عرض علامات التحذير من الحصص"; "show_quota_warning_markers_subtitle" = "ارسم علامات عتبة على أشرطة الاستخدام عند تكوين تحذيرات الحصص."; +"show_pace_title" = "إظهار وتيرة الاستخدام"; +"show_pace_subtitle" = "يعرض شريط التقدم أو التأخر ونص التوقعات على أشرطة الاستخدام."; "weekly_progress_work_days_title" = "أيام العمل الأسبوعية للتقدم"; "weekly_progress_work_days_subtitle" = "حدد أيام عمل لمؤشرات شريط الاستخدام الأسبوعي وحسابات السرعة."; "workday_tick_appearance_title" = "مظهر علامات أيام العمل"; diff --git a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings index 4510c62502..5ea594bb77 100644 --- a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings @@ -601,6 +601,8 @@ "display_mode_subtitle" = "Trieu què es mostra a la barra de menús (Ritme mostra l'ús respecte al previst)."; "show_quota_warning_markers_title" = "Mostreu els marcadors d'avís de quota"; "show_quota_warning_markers_subtitle" = "Dibuixa marques de llindar a les barres d'ús quan hi ha avisos de quota configurats."; +"show_pace_title" = "Mostra el ritme"; +"show_pace_subtitle" = "Mostra la franja d'avançament o retard i el text de previsió a les barres d'ús."; "weekly_progress_work_days_title" = "Dies laborables del progrés setmanal"; "weekly_progress_work_days_subtitle" = "Definiu els dies laborables per als marcadors de les barres d'ús setmanal i els càlculs de ritme."; "workday_tick_appearance_title" = "Aparença de les marques dels dies laborables"; diff --git a/Sources/CodexBar/Resources/de.lproj/Localizable.strings b/Sources/CodexBar/Resources/de.lproj/Localizable.strings index 42de997c91..6a95f70fd7 100644 --- a/Sources/CodexBar/Resources/de.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/de.lproj/Localizable.strings @@ -615,6 +615,8 @@ "display_mode_subtitle" = "Wählen Sie aus, was in der Menüleiste angezeigt werden soll (Pace zeigt die Nutzung im Vergleich zur erwarteten)."; "show_quota_warning_markers_title" = "Quotenwarnmarkierungen anzeigen"; "show_quota_warning_markers_subtitle" = "Zeichnen Sie Schwellenwertmarkierungen auf Nutzungsbalken, wenn Kontingentwarnungen konfiguriert sind."; +"show_pace_title" = "Tempo anzeigen"; +"show_pace_subtitle" = "Zeigt den Streifen für Vorsprung oder Rückstand und den Prognosetext auf den Nutzungsbalken."; "weekly_progress_work_days_title" = "Wöchentliche Fortschrittsarbeitstage"; "weekly_progress_work_days_subtitle" = "Legt Arbeitstage für Markierungen in wöchentlichen Nutzungsbalken und Tempo-Berechnungen fest."; "workday_tick_appearance_title" = "Darstellung der Arbeitstagsmarkierungen"; diff --git a/Sources/CodexBar/Resources/en.lproj/Localizable.strings b/Sources/CodexBar/Resources/en.lproj/Localizable.strings index 5238ac5188..62fefa4b41 100644 --- a/Sources/CodexBar/Resources/en.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/en.lproj/Localizable.strings @@ -603,6 +603,8 @@ "display_mode_subtitle" = "Choose what to show in the menu bar (Pace shows usage vs. expected)."; "show_quota_warning_markers_title" = "Show quota warning markers"; "show_quota_warning_markers_subtitle" = "Draw threshold tick marks on usage bars when quota warnings are configured."; +"show_pace_title" = "Show pace"; +"show_pace_subtitle" = "Show the ahead/behind stripe and forecast text on usage bars."; "weekly_progress_work_days_title" = "Work days"; "weekly_progress_work_days_subtitle" = "Set work days for weekly usage-bar markers and pace calculations."; "workday_tick_appearance_title" = "Workday tick appearance"; diff --git a/Sources/CodexBar/Resources/es.lproj/Localizable.strings b/Sources/CodexBar/Resources/es.lproj/Localizable.strings index fc76977d25..4ea0cbfbce 100644 --- a/Sources/CodexBar/Resources/es.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/es.lproj/Localizable.strings @@ -609,6 +609,8 @@ "display_mode_subtitle" = "Elige qué mostrar en la barra de menús (Ritmo muestra el uso frente al previsto)."; "show_quota_warning_markers_title" = "Mostrar marcadores de aviso de cuota"; "show_quota_warning_markers_subtitle" = "Dibuja marcas de umbral en las barras de uso cuando hay avisos de cuota configurados."; +"show_pace_title" = "Mostrar el ritmo"; +"show_pace_subtitle" = "Muestra la franja de adelanto o retraso y el texto de previsión en las barras de uso."; "weekly_progress_work_days_title" = "Días laborables del progreso semanal"; "weekly_progress_work_days_subtitle" = "Define los días laborables para los marcadores de las barras de uso semanal y los cálculos de ritmo."; "workday_tick_appearance_title" = "Aspecto de las marcas de días laborables"; diff --git a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings index 62189fb0fe..04982bf3fd 100644 --- a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings @@ -623,6 +623,8 @@ "display_mode_subtitle" = "انتخاب کنید که در نوار منو چه چیزی نمایش داده شود (Pace میزان مصرف در مقابل مورد انتظار را نشان می دهد)."; "show_quota_warning_markers_title" = "نشانگرهای هشدار سهمیه را نشان دهید"; "show_quota_warning_markers_subtitle" = "علامت تیک آستانه را روی نوارهای استفاده هنگام پیکربندی هشدارهای سهمیه رسم کنید."; +"show_pace_title" = "نمایش آهنگ مصرف"; +"show_pace_subtitle" = "نوار جلوتر یا عقب‌تر بودن و متن پیش‌بینی را روی نوارهای مصرف نشان می‌دهد."; "weekly_progress_work_days_title" = "روزهای کاری پیشرفت هفتگی"; "weekly_progress_work_days_subtitle" = "روزهای کاری را برای نشانگرهای نوار مصرف هفتگی و محاسبات سرعت تعیین کنید."; "workday_tick_appearance_title" = "نمایش نشانه‌های روز کاری"; diff --git a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings index b64d14827b..da6e117695 100644 --- a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings @@ -617,6 +617,8 @@ "display_mode_subtitle" = "Choisissez ce que vous voulez afficher dans la barre de menu (Pace affiche l'utilisation par rapport à celle attendue)."; "show_quota_warning_markers_title" = "Afficher les marqueurs d'avertissement de quota"; "show_quota_warning_markers_subtitle" = "Dessinez des coches de seuil sur les barres d’utilisation lorsque des avertissements de quota sont configurés."; +"show_pace_title" = "Afficher le rythme"; +"show_pace_subtitle" = "Affiche la bande d'avance ou de retard et le texte de prévision sur les barres d'utilisation."; "weekly_progress_work_days_title" = "Jours de travail hebdomadaires"; "weekly_progress_work_days_subtitle" = "Définit les jours ouvrés pour les repères des barres d’utilisation hebdomadaire et le calcul du rythme."; "workday_tick_appearance_title" = "Apparence des repères de jours ouvrés"; diff --git a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings index 1162c1d26b..1be1e0f687 100644 --- a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings @@ -596,6 +596,8 @@ "display_mode_subtitle" = "Escolle que amosar na barra de menús (Ritmo amosa o uso fronte ao previsto)."; "show_quota_warning_markers_title" = "Amosar os marcadores de aviso de cota"; "show_quota_warning_markers_subtitle" = "Debuxa marcas de limiar nas barras de uso cando hai avisos de cota configurados."; +"show_pace_title" = "Amosar o ritmo"; +"show_pace_subtitle" = "Amosa a franxa de adianto ou atraso e o texto de previsión nas barras de uso."; "weekly_progress_work_days_title" = "Días laborables do progreso semanal"; "weekly_progress_work_days_subtitle" = "Define os días laborables para os marcadores das barras de uso semanal e os cálculos de ritmo."; "workday_tick_appearance_title" = "Aspecto das marcas dos días laborables"; diff --git a/Sources/CodexBar/Resources/id.lproj/Localizable.strings b/Sources/CodexBar/Resources/id.lproj/Localizable.strings index 019de1ca5b..0655d21fcc 100644 --- a/Sources/CodexBar/Resources/id.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/id.lproj/Localizable.strings @@ -625,6 +625,8 @@ "display_mode_subtitle" = "Pilih apa yang ditampilkan di menu bar (Pace menampilkan penggunaan vs. perkiraan)."; "show_quota_warning_markers_title" = "Tampilkan penanda peringatan kuota"; "show_quota_warning_markers_subtitle" = "Gambar tanda centang ambang batas pada bilah penggunaan saat peringatan kuota dikonfigurasi."; +"show_pace_title" = "Tampilkan laju"; +"show_pace_subtitle" = "Menampilkan garis unggul atau tertinggal dan teks perkiraan pada bilah penggunaan."; "weekly_progress_work_days_title" = "Hari kerja progres mingguan"; "weekly_progress_work_days_subtitle" = "Atur hari kerja untuk penanda bilah penggunaan mingguan dan perhitungan pace."; "workday_tick_appearance_title" = "Tampilan tanda hari kerja"; diff --git a/Sources/CodexBar/Resources/it.lproj/Localizable.strings b/Sources/CodexBar/Resources/it.lproj/Localizable.strings index 4be718eb18..7055d3bc59 100644 --- a/Sources/CodexBar/Resources/it.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/it.lproj/Localizable.strings @@ -625,6 +625,8 @@ "display_mode_subtitle" = "Scegli cosa mostrare nella barra menu (Andamento confronta utilizzo e atteso)."; "show_quota_warning_markers_title" = "Mostra indicatori avviso quota"; "show_quota_warning_markers_subtitle" = "Disegna tacche soglia sulle barre quando gli avvisi quota sono configurati."; +"show_pace_title" = "Mostra il ritmo"; +"show_pace_subtitle" = "Mostra la striscia di anticipo o ritardo e il testo di previsione sulle barre di utilizzo."; "weekly_progress_work_days_title" = "Giorni lavorativi progresso settimanale"; "weekly_progress_work_days_subtitle" = "Disegna i confini giornalieri sulle barre settimanali."; "workday_tick_appearance_title" = "Aspetto delle tacche dei giorni lavorativi"; diff --git a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings index a61ccaec8e..69db7dd75a 100644 --- a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings @@ -614,6 +614,8 @@ "display_mode_subtitle" = "メニューバーに表示する内容を選択します(ペースは使用量と想定値の比較を表示します)。"; "show_quota_warning_markers_title" = "クォータ警告マーカーを表示"; "show_quota_warning_markers_subtitle" = "クォータ警告が設定されている場合、使用量バーにしきい値の目盛りを描画します。"; +"show_pace_title" = "ペースを表示"; +"show_pace_subtitle" = "使用状況バーに、予定より進んでいるか遅れているかを示す線と予測テキストを表示します。"; "weekly_progress_work_days_title" = "週間進捗の作業日"; "weekly_progress_work_days_subtitle" = "週間使用量バーの目盛りとペース計算に使用する作業日を設定します。"; "workday_tick_appearance_title" = "稼働日の目盛りの表示"; diff --git a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings index 3a88f53bc7..966c957b34 100644 --- a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings @@ -607,6 +607,8 @@ "display_mode_subtitle" = "메뉴 막대에 표시할 내용을 선택하세요(사용 속도는 사용량 대 예상치를 표시)."; "show_quota_warning_markers_title" = "할당량 경고 표시기 표시"; "show_quota_warning_markers_subtitle" = "할당량 경고가 구성된 경우 사용량 막대에 임계값 눈금 표시를 그립니다."; +"show_pace_title" = "사용 속도 표시"; +"show_pace_subtitle" = "사용량 막대에 예상보다 빠른지 느린지를 나타내는 선과 예측 텍스트를 표시합니다."; "weekly_progress_work_days_title" = "주간 진행률 근무일"; "weekly_progress_work_days_subtitle" = "주간 사용량 막대 눈금과 페이스 계산에 사용할 근무일을 설정합니다."; "workday_tick_appearance_title" = "근무일 눈금 모양"; diff --git a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings index d645590e5c..23fc696aac 100644 --- a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings @@ -617,6 +617,8 @@ "display_mode_subtitle" = "Kies wat u wilt weergeven in de menubalk (Tempo toont gebruik vs. verwacht)."; "show_quota_warning_markers_title" = "Toon waarschuwingsmarkeringen voor quota"; "show_quota_warning_markers_subtitle" = "Teken drempelmarkeringen op gebruiksbalken wanneer quotawaarschuwingen zijn geconfigureerd."; +"show_pace_title" = "Tempo tonen"; +"show_pace_subtitle" = "Toont de streep voor voor- of achterstand en de prognosetekst op de gebruiksbalken."; "weekly_progress_work_days_title" = "Wekelijkse voortgang werkdagen"; "weekly_progress_work_days_subtitle" = "Stel werkdagen in voor markeringen op wekelijkse gebruiksbalken en tempoberekeningen."; "workday_tick_appearance_title" = "Weergave van werkdagmarkeringen"; diff --git a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings index cbfba3c308..1278b23f75 100644 --- a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings @@ -625,6 +625,8 @@ "display_mode_subtitle" = "Wybierz, co pokazywać na pasku menu (Tempo pokazuje użycie względem oczekiwanego)."; "show_quota_warning_markers_title" = "Pokaż znaczniki ostrzeżeń limitu"; "show_quota_warning_markers_subtitle" = "Rysuje znaczniki progów na paskach użycia, gdy skonfigurowano ostrzeżenia limitu."; +"show_pace_title" = "Pokaż tempo"; +"show_pace_subtitle" = "Pokazuje pasek wyprzedzenia lub opóźnienia oraz tekst prognozy na paskach użycia."; "weekly_progress_work_days_title" = "Dni robocze postępu tygodniowego"; "weekly_progress_work_days_subtitle" = "Rysuje znaczniki granic dni na tygodniowych paskach użycia."; "workday_tick_appearance_title" = "Wygląd znaczników dni roboczych"; diff --git a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings index ea48caa51c..3b2861a7d4 100644 --- a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings @@ -614,6 +614,8 @@ "display_mode_subtitle" = "Escolha o que mostrar na barra de menus (Ritmo mostra uso vs. esperado)."; "show_quota_warning_markers_title" = "Mostrar marcadores de alerta de cota"; "show_quota_warning_markers_subtitle" = "Desenha marcas de limite nas barras de uso quando os alertas de cota estão configurados."; +"show_pace_title" = "Mostrar o ritmo"; +"show_pace_subtitle" = "Mostra a faixa de adiantamento ou atraso e o texto de previsão nas barras de uso."; "weekly_progress_work_days_title" = "Dias úteis no progresso semanal"; "weekly_progress_work_days_subtitle" = "Define os dias úteis para marcadores das barras de uso semanal e cálculos de ritmo."; "workday_tick_appearance_title" = "Aparência das marcas de dias úteis"; diff --git a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings index 72835ca1bd..a57313121e 100644 --- a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings @@ -618,6 +618,8 @@ "display_mode_subtitle" = "Выберите, что показывать в строке меню: темп сравнивает фактическое использование с ожидаемым."; "show_quota_warning_markers_title" = "Показывать маркеры предупреждений о квоте"; "show_quota_warning_markers_subtitle" = "Рисует отметки порогов на индикаторах использования, если настроены предупреждения о квотах."; +"show_pace_title" = "Показывать темп"; +"show_pace_subtitle" = "Показывает полосу опережения или отставания и текст прогноза на полосах использования."; "weekly_progress_work_days_title" = "Рабочие дни для недельного прогресса"; "weekly_progress_work_days_subtitle" = "Задаёт рабочие дни для недельных отметок использования и расчёта темпа."; "workday_tick_appearance_title" = "Вид отметок рабочих дней"; diff --git a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings index 8dd627328a..e12d53e6b4 100644 --- a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings @@ -616,6 +616,8 @@ "display_mode_subtitle" = "Välj vad som ska visas i menyraden (takt visar användning mot förväntat)."; "show_quota_warning_markers_title" = "Visa kvotvarningsmarkörer"; "show_quota_warning_markers_subtitle" = "Rita tröskelmarkeringar på användningsstaplar när kvotvarningar är konfigurerade."; +"show_pace_title" = "Visa takt"; +"show_pace_subtitle" = "Visar remsan för försprång eller eftersläpning och prognostexten på användningsstaplarna."; "weekly_progress_work_days_title" = "Arbetsdagar i veckoförlopp"; "weekly_progress_work_days_subtitle" = "Ställ in arbetsdagar för markeringar i veckostaplar och tempoberäkningar."; "workday_tick_appearance_title" = "Utseende för arbetsdagsmarkeringar"; diff --git a/Sources/CodexBar/Resources/th.lproj/Localizable.strings b/Sources/CodexBar/Resources/th.lproj/Localizable.strings index e4dc22c0ee..bdcb5834d9 100644 --- a/Sources/CodexBar/Resources/th.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/th.lproj/Localizable.strings @@ -623,6 +623,8 @@ "display_mode_subtitle" = "เลือกสิ่งที่จะแสดงในแถบเมนู (อัตราก้าวแสดงการใช้งานเทียบกับที่คาดไว้)"; "show_quota_warning_markers_title" = "แสดงเครื่องหมายเตือนโควต้า"; "show_quota_warning_markers_subtitle" = "วาดเครื่องหมายถูกเกณฑ์บนแถบการใช้งานเมื่อมีการกําหนดค่าคําเตือนโควต้า"; +"show_pace_title" = "แสดงอัตราการใช้งาน"; +"show_pace_subtitle" = "แสดงแถบบอกว่าเร็วหรือช้ากว่ากำหนด และข้อความคาดการณ์บนแถบการใช้งาน"; "weekly_progress_work_days_title" = "วันทํางานความคืบหน้ารายสัปดาห์"; "weekly_progress_work_days_subtitle" = "กําหนดวันทํางานสําหรับเครื่องหมายแถบการใช้งานรายสัปดาห์และการคํานวณความเร็ว"; "workday_tick_appearance_title" = "ลักษณะขีดวันทำงาน"; diff --git a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings index 0c7b4bf197..2a19dbce2f 100644 --- a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings @@ -623,6 +623,8 @@ "display_mode_subtitle" = "Menü çubuğunda ne gösterileceğini seçin (Hız, kullanımı beklentiyle karşılaştırır)."; "show_quota_warning_markers_title" = "Kota uyarı işaretlerini göster"; "show_quota_warning_markers_subtitle" = "Kota uyarıları yapılandırıldığında kullanım çubuklarına eşik çizgileri çizer."; +"show_pace_title" = "Hızı göster"; +"show_pace_subtitle" = "Kullanım çubuklarında öndeyken veya gerideyken görünen şeridi ve tahmin metnini gösterir."; "weekly_progress_work_days_title" = "Haftalık ilerleme iş günleri"; "weekly_progress_work_days_subtitle" = "Haftalık kullanım çubuğu işaretleri ve tempo hesaplamaları için iş günlerini ayarlar."; "workday_tick_appearance_title" = "İş günü işaretlerinin görünümü"; diff --git a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings index 7eccdc8802..787098b6f0 100644 --- a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings @@ -617,6 +617,8 @@ "display_mode_subtitle" = "Виберіть, що відображати на панелі меню (Pace показує використання порівняно з очікуваним)."; "show_quota_warning_markers_title" = "Показати маркери попередження про квоти"; "show_quota_warning_markers_subtitle" = "Малюйте порогові позначки на панелях використання, коли налаштовано попередження про квоту."; +"show_pace_title" = "Показувати темп"; +"show_pace_subtitle" = "Показує смугу випередження або відставання та текст прогнозу на смугах використання."; "weekly_progress_work_days_title" = "Щотижневі робочі дні"; "weekly_progress_work_days_subtitle" = "Задайте робочі дні для позначок на смугах тижневого використання та розрахунків темпу."; "workday_tick_appearance_title" = "Вигляд позначок робочих днів"; diff --git a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings index f7b8cee79a..13e4b7693c 100644 --- a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings @@ -613,6 +613,8 @@ "display_mode_subtitle" = "Chọn nội dung sẽ hiển thị trong thanh menu (Tốc độ hiển thị Mức sử dụng so với dự kiến)."; "show_quota_warning_markers_title" = "Hiển thị Hạn mức dấu cảnh báo"; "show_quota_warning_markers_subtitle" = "Vẽ dấu kiểm ngưỡng trên thanh Mức sử dụng khi cảnh báo Hạn mức được định cấu hình."; +"show_pace_title" = "Hiển thị nhịp độ"; +"show_pace_subtitle" = "Hiển thị vạch vượt trước hoặc chậm hơn và văn bản dự báo trên thanh sử dụng."; "weekly_progress_work_days_title" = "Tiến độ ngày làm việc hàng tuần"; "weekly_progress_work_days_subtitle" = "Đặt ngày làm việc cho các vạch trên thanh sử dụng hằng tuần và phép tính nhịp độ."; "workday_tick_appearance_title" = "Kiểu vạch ngày làm việc"; diff --git a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings index a1a0b93a15..59b95da728 100644 --- a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings @@ -599,6 +599,8 @@ "display_mode_subtitle" = "选择菜单栏中显示的内容(进度会显示实际用量与预期的对比)。"; "show_quota_warning_markers_title" = "显示配额预警标记"; "show_quota_warning_markers_subtitle" = "配置配额预警后,在用量条上绘制阈值刻度标记。"; +"show_pace_title" = "显示用量节奏"; +"show_pace_subtitle" = "在用量条上显示领先或落后的标记线和预测文本。"; "show_provider_changelog_links_title" = "显示提供商变更日志链接"; "show_provider_changelog_links_subtitle" = "在菜单中为支持的 CLI 提供商添加发布说明链接。"; "show_credits_extra_usage_title" = "显示额度 + 额外用量"; diff --git a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings index ad957338aa..371a194ad4 100644 --- a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings @@ -616,6 +616,8 @@ "display_mode_subtitle" = "選擇選單列要顯示的內容(進度會比較目前用量和時間進度)。"; "show_quota_warning_markers_title" = "顯示配額提醒標記"; "show_quota_warning_markers_subtitle" = "設定配額提醒後,在使用量條上繪製門檻刻度標記。"; +"show_pace_title" = "顯示用量節奏"; +"show_pace_subtitle" = "在用量條上顯示領先或落後的標記線和預測文字。"; "weekly_progress_work_days_title" = "每週進度工作日標記"; "weekly_progress_work_days_subtitle" = "設定用於每週用量條刻度與進度計算的工作日。"; "workday_tick_appearance_title" = "工作日刻度外觀"; diff --git a/Sources/CodexBar/SettingsStore+Defaults.swift b/Sources/CodexBar/SettingsStore+Defaults.swift index 9c35d38533..29ae5a6cf1 100644 --- a/Sources/CodexBar/SettingsStore+Defaults.swift +++ b/Sources/CodexBar/SettingsStore+Defaults.swift @@ -257,6 +257,14 @@ extension SettingsStore { } } + var paceVisible: Bool { + get { self.defaultsState.paceVisible } + set { + self.defaultsState.paceVisible = newValue + self.userDefaults.set(newValue, forKey: "paceVisible") + } + } + var weeklyProgressWorkDays: Int? { get { self.defaultsState.weeklyProgressWorkDays } set { diff --git a/Sources/CodexBar/SettingsStore+MenuObservation.swift b/Sources/CodexBar/SettingsStore+MenuObservation.swift index d144a963a5..4461b32a0b 100644 --- a/Sources/CodexBar/SettingsStore+MenuObservation.swift +++ b/Sources/CodexBar/SettingsStore+MenuObservation.swift @@ -22,6 +22,7 @@ extension SettingsStore { _ = self.quotaWarningSoundEnabled _ = self.quotaWarningOnScreenAlertEnabled _ = self.quotaWarningMarkersVisible + _ = self.paceVisible _ = self.weeklyProgressWorkDays _ = self.workdayTickAppearance _ = self.usageBarsShowUsed diff --git a/Sources/CodexBar/SettingsStore+Sync.swift b/Sources/CodexBar/SettingsStore+Sync.swift index 058fe63fed..16cf83f10e 100644 --- a/Sources/CodexBar/SettingsStore+Sync.swift +++ b/Sources/CodexBar/SettingsStore+Sync.swift @@ -56,6 +56,7 @@ extension SettingsStore { quotaWarningSoundEnabled: self.quotaWarningSoundEnabled, quotaWarningOnScreenAlertEnabled: self.quotaWarningOnScreenAlertEnabled, quotaWarningMarkersVisible: self.quotaWarningMarkersVisible, + paceVisible: self.paceVisible, weeklyProgressWorkDays: self.weeklyProgressWorkDays, workdayTickAppearance: self.workdayTickAppearance.rawValue, usageBarsShowUsed: self.usageBarsShowUsed, @@ -89,6 +90,9 @@ extension SettingsStore { self.quotaWarningSoundEnabled = preferences.quotaWarningSoundEnabled self.quotaWarningOnScreenAlertEnabled = preferences.quotaWarningOnScreenAlertEnabled self.quotaWarningMarkersVisible = preferences.quotaWarningMarkersVisible + if let paceVisible = preferences.paceVisible { + self.paceVisible = paceVisible + } self.weeklyProgressWorkDays = preferences.weeklyProgressWorkDays if let rawAppearance = preferences.workdayTickAppearance, let appearance = WorkdayTickAppearance(rawValue: rawAppearance) diff --git a/Sources/CodexBar/SettingsStore.swift b/Sources/CodexBar/SettingsStore.swift index c57dc8fd06..f805cae43c 100644 --- a/Sources/CodexBar/SettingsStore.swift +++ b/Sources/CodexBar/SettingsStore.swift @@ -510,6 +510,11 @@ extension SettingsStore { if Self.isRunningTests, quotaWarningMarkersVisibleDefault == nil { userDefaults.set(true, forKey: "quotaWarningMarkersVisible") } + let paceVisibleDefault = userDefaults.object(forKey: "paceVisible") as? Bool + let paceVisible = paceVisibleDefault ?? true + if Self.isRunningTests, paceVisibleDefault == nil { + userDefaults.set(true, forKey: "paceVisible") + } let weeklyProgressWorkDays = userDefaults.object(forKey: "weeklyProgressWorkDays") as? Int let workdayTickAppearanceRaw = userDefaults.string(forKey: "workdayTickAppearance") ?? WorkdayTickAppearance.subtle.rawValue @@ -660,6 +665,7 @@ extension SettingsStore { quotaWarningSoundEnabled: quotaWarnings.soundEnabled, quotaWarningOnScreenAlertEnabled: quotaWarnings.onScreenAlertEnabled, quotaWarningMarkersVisible: quotaWarningMarkersVisible, + paceVisible: paceVisible, weeklyProgressWorkDays: weeklyProgressWorkDays, workdayTickAppearanceRaw: workdayTickAppearanceRaw, usageBarsShowUsed: usageBarsShowUsed, diff --git a/Sources/CodexBar/SettingsStoreState.swift b/Sources/CodexBar/SettingsStoreState.swift index b1547135c2..b5e994e621 100644 --- a/Sources/CodexBar/SettingsStoreState.swift +++ b/Sources/CodexBar/SettingsStoreState.swift @@ -23,6 +23,7 @@ struct SettingsDefaultsState { var quotaWarningSoundEnabled: Bool var quotaWarningOnScreenAlertEnabled: Bool var quotaWarningMarkersVisible: Bool + var paceVisible: Bool var weeklyProgressWorkDays: Int? var workdayTickAppearanceRaw: String var usageBarsShowUsed: Bool diff --git a/Sources/CodexBar/StatusItemController+MenuCardModel.swift b/Sources/CodexBar/StatusItemController+MenuCardModel.swift index 1f50122e8b..519d668d50 100644 --- a/Sources/CodexBar/StatusItemController+MenuCardModel.swift +++ b/Sources/CodexBar/StatusItemController+MenuCardModel.swift @@ -157,6 +157,7 @@ extension StatusItemController { ], workDaysPerWeek: self.settings.weeklyProgressWorkDays, workdayTickAppearance: self.settings.workdayTickAppearance, + paceVisible: self.settings.paceVisible, usesLiveSubtitle: surface == .liveCard, preferredCurrencyCode: self.settings.preferredCurrencyCode, now: now) diff --git a/Sources/CodexBarCore/Sync/SyncModels.swift b/Sources/CodexBarCore/Sync/SyncModels.swift index 373971e67e..b814d435cb 100644 --- a/Sources/CodexBarCore/Sync/SyncModels.swift +++ b/Sources/CodexBarCore/Sync/SyncModels.swift @@ -240,6 +240,7 @@ public struct SyncedPreferences: Codable, Sendable { public var quotaWarningSoundEnabled: Bool public var quotaWarningOnScreenAlertEnabled: Bool public var quotaWarningMarkersVisible: Bool + public var paceVisible: Bool? public var weeklyProgressWorkDays: Int? public var workdayTickAppearance: String? public var usageBarsShowUsed: Bool @@ -272,6 +273,7 @@ public struct SyncedPreferences: Codable, Sendable { quotaWarningSoundEnabled: Bool, quotaWarningOnScreenAlertEnabled: Bool, quotaWarningMarkersVisible: Bool, + paceVisible: Bool? = nil, weeklyProgressWorkDays: Int?, workdayTickAppearance: String? = nil, usageBarsShowUsed: Bool, @@ -303,6 +305,7 @@ public struct SyncedPreferences: Codable, Sendable { self.quotaWarningSoundEnabled = quotaWarningSoundEnabled self.quotaWarningOnScreenAlertEnabled = quotaWarningOnScreenAlertEnabled self.quotaWarningMarkersVisible = quotaWarningMarkersVisible + self.paceVisible = paceVisible self.weeklyProgressWorkDays = weeklyProgressWorkDays self.workdayTickAppearance = workdayTickAppearance self.usageBarsShowUsed = usageBarsShowUsed diff --git a/Tests/CodexBarTests/CloudSyncSettingsTests.swift b/Tests/CodexBarTests/CloudSyncSettingsTests.swift index c9b3b12d56..6b090461fa 100644 --- a/Tests/CodexBarTests/CloudSyncSettingsTests.swift +++ b/Tests/CodexBarTests/CloudSyncSettingsTests.swift @@ -66,6 +66,27 @@ struct CloudSyncSettingsTests { #expect(decoded.preferences.workdayTickAppearance == nil) } + @Test + func `legacy synced preferences without pace visibility decode compatibly`() throws { + let fixture = try self.makeFixture("legacy-pace-visible") + let payload = PreferencesSyncPayload(preferences: fixture.store.syncedPreferences) + let encoded = try CanonicalSyncJSON.encode(payload) + var object = try #require(JSONSerialization.jsonObject(with: encoded) as? [String: Any]) + var preferences = try #require(object["preferences"] as? [String: Any]) + preferences.removeValue(forKey: "paceVisible") + object["preferences"] = preferences + let legacyData = try JSONSerialization.data(withJSONObject: object) + + let decoded = try CanonicalSyncJSON.decode(PreferencesSyncPayload.self, from: legacyData) + + #expect(decoded.preferences.paceVisible == nil) + + // An absent key must leave the local value untouched, not reset it. + fixture.store.paceVisible = false + fixture.store.applySyncedPreferences(decoded.preferences) + #expect(fixture.store.paceVisible == false) + } + @Test func `config watcher suppresses self writes and observes external atomic replacement`() async throws { let directory = FileManager.default.temporaryDirectory diff --git a/Tests/CodexBarTests/PaceVisibilityScreenshotRenderTests.swift b/Tests/CodexBarTests/PaceVisibilityScreenshotRenderTests.swift new file mode 100644 index 0000000000..a42281951f --- /dev/null +++ b/Tests/CodexBarTests/PaceVisibilityScreenshotRenderTests.swift @@ -0,0 +1,105 @@ +import AppKit +import CodexBarCore +import SwiftUI +import XCTest +@testable import CodexBar + +/// Developer tool, skipped by default: renders a provider card with the pace +/// stripe and forecast text shown and hidden, for documentation and PR review. +/// +/// Run with: +/// CODEXBAR_PACE_SCREENSHOT_DIR=~/Downloads swift test --filter PaceVisibilityScreenshotRenderTests +@MainActor +final class PaceVisibilityScreenshotRenderTests: XCTestCase { + private static let width: CGFloat = 320 + private static let now = Date(timeIntervalSince1970: 1_782_000_000) + + func test_renderPaceVisibilityScreenshots() throws { + guard let dir = ProcessInfo.processInfo.environment["CODEXBAR_PACE_SCREENSHOT_DIR"] else { + throw XCTSkip("Set CODEXBAR_PACE_SCREENSHOT_DIR to render pace visibility screenshots.") + } + let expanded = NSString(string: dir).expandingTildeInPath + let directory = URL(fileURLWithPath: expanded, isDirectory: true) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + + for (name, paceVisible) in [("pace-on-before", true), ("pace-off-after", false)] { + let model = UsageMenuCardView.Model.make(Self.input(paceVisible: paceVisible)) + let view = AnyView(UsageMenuCardView(model: model, width: Self.width) + .padding(12) + .background(Color(nsColor: .windowBackgroundColor))) + let data = try XCTUnwrap(Self.pngData(for: view), "render failed for \(name)") + let url = directory.appendingPathComponent("codexbar-\(name).png") + try data.write(to: url, options: .atomic) + print("Wrote \(url.path)") + } + } + + /// A Claude account running ahead of the sustainable rate, so the stripe and + /// the forecast text both have something to render. + private static func input(paceVisible: Bool) -> UsageMenuCardView.Model.Input { + let snapshot = UsageSnapshot( + primary: RateWindow( + usedPercent: 61, + windowMinutes: 300, + resetsAt: Self.now.addingTimeInterval(3 * 3600 + 12 * 60), + resetDescription: nil), + secondary: RateWindow( + usedPercent: 44, + windowMinutes: 10080, + resetsAt: Self.now.addingTimeInterval(4 * 86400 + 22 * 3600), + resetDescription: nil), + updatedAt: Self.now, + identity: ProviderIdentitySnapshot( + providerID: .claude, + accountEmail: "you@example.com", + accountOrganization: nil, + loginMethod: "Max 5x")) + return .init( + provider: .claude, + metadata: ProviderDefaults.metadata[.claude]!, + snapshot: snapshot, + credits: nil, + creditsError: nil, + dashboard: nil, + dashboardError: nil, + tokenSnapshot: nil, + tokenError: nil, + account: AccountInfo(email: "you@example.com", plan: "Max 5x"), + isRefreshing: false, + lastError: nil, + usageBarsShowUsed: true, + resetTimeDisplayStyle: .countdown, + tokenCostUsageEnabled: false, + showOptionalCreditsAndExtraUsage: true, + hidePersonalInfo: true, + paceVisible: paceVisible, + now: Self.now) + } + + private static func pngData(for view: AnyView) -> Data? { + let hosting = NSHostingView(rootView: view) + hosting.appearance = NSAppearance(named: .darkAqua) + let size = hosting.fittingSize + guard size.width > 0, size.height > 0 else { return nil } + hosting.frame = CGRect(origin: .zero, size: size) + hosting.layoutSubtreeIfNeeded() + + let scale: CGFloat = 2 + guard let representation = NSBitmapImageRep( + bitmapDataPlanes: nil, + pixelsWide: Int(size.width * scale), + pixelsHigh: Int(size.height * scale), + bitsPerSample: 8, + samplesPerPixel: 4, + hasAlpha: true, + isPlanar: false, + colorSpaceName: .deviceRGB, + bytesPerRow: 0, + bitsPerPixel: 0) + else { return nil } + representation.size = size + guard let context = NSGraphicsContext(bitmapImageRep: representation) else { return nil } + hosting.displayIgnoringOpacity(hosting.bounds, in: context) + return representation.representation(using: .png, properties: [:]) + } +} diff --git a/Tests/CodexBarTests/PaceVisibilityTests.swift b/Tests/CodexBarTests/PaceVisibilityTests.swift new file mode 100644 index 0000000000..9b517110e3 --- /dev/null +++ b/Tests/CodexBarTests/PaceVisibilityTests.swift @@ -0,0 +1,263 @@ +import CodexBarCore +import Foundation +import Testing +@testable import CodexBar + +struct PaceVisibilityTests { + /// Claude session window that is well ahead of the sustainable rate, so the + /// pace stripe and the forecast text both have something to render. + private static func offPaceInput( + now: Date, + metadata: ProviderMetadata, + paceVisible: Bool, + hidePersonalInfo: Bool = false) -> UsageMenuCardView.Model.Input + { + let snapshot = UsageSnapshot( + primary: RateWindow( + usedPercent: 60, + windowMinutes: 300, + resetsAt: now.addingTimeInterval(4 * 3600), + resetDescription: nil), + secondary: RateWindow( + usedPercent: 70, + windowMinutes: 10080, + resetsAt: now.addingTimeInterval(5 * 86400), + resetDescription: nil), + updatedAt: now, + identity: ProviderIdentitySnapshot( + providerID: .claude, + accountEmail: "claude@example.com", + accountOrganization: nil, + loginMethod: "Max")) + return .init( + provider: .claude, + metadata: metadata, + snapshot: snapshot, + credits: nil, + creditsError: nil, + dashboard: nil, + dashboardError: nil, + tokenSnapshot: nil, + tokenError: nil, + account: AccountInfo(email: "claude@example.com", plan: "Max"), + isRefreshing: false, + lastError: nil, + usageBarsShowUsed: true, + resetTimeDisplayStyle: .countdown, + tokenCostUsageEnabled: false, + showOptionalCreditsAndExtraUsage: true, + hidePersonalInfo: hidePersonalInfo, + paceVisible: paceVisible, + now: now) + } + + @Test + func `pace stripe and text render when pace is visible`() throws { + let now = Date() + let metadata = try #require(ProviderDefaults.metadata[.claude]) + let model = UsageMenuCardView.Model.make( + Self.offPaceInput(now: now, metadata: metadata, paceVisible: true)) + + let primary = try #require(model.metrics.first { $0.id == "primary" }) + #expect(primary.pacePercent != nil) + #expect(primary.detailLeftText != nil) + } + + @Test + func `hiding pace clears the stripe and the forecast text`() throws { + let now = Date() + let metadata = try #require(ProviderDefaults.metadata[.claude]) + let model = UsageMenuCardView.Model.make( + Self.offPaceInput(now: now, metadata: metadata, paceVisible: false)) + + #expect(!model.metrics.isEmpty) + for metric in model.metrics { + #expect(metric.pacePercent == nil) + #expect(metric.detailLeftText == nil) + #expect(metric.detailRightText == nil) + #expect(metric.sessionEquivalentDetail == nil) + } + } + + /// The primary lane is built from `PrimaryMetricPresentation` rather than a + /// `PaceDetail`, so it takes a different path than the secondary lane. + @Test + func `hiding pace clears the primary metric specifically`() throws { + let now = Date() + let metadata = try #require(ProviderDefaults.metadata[.claude]) + let model = UsageMenuCardView.Model.make( + Self.offPaceInput(now: now, metadata: metadata, paceVisible: false)) + + let primary = try #require(model.metrics.first { $0.id == "primary" }) + #expect(primary.pacePercent == nil) + #expect(primary.detailLeftText == nil) + #expect(primary.detailRightText == nil) + } + + /// Guards the interaction with `redactedMetrics`, which rebuilds every + /// metric field by field when personal info is hidden. + @Test + func `hiding pace also applies when personal info is hidden`() throws { + let now = Date() + let metadata = try #require(ProviderDefaults.metadata[.claude]) + let model = UsageMenuCardView.Model.make(Self.offPaceInput( + now: now, + metadata: metadata, + paceVisible: false, + hidePersonalInfo: true)) + + let primary = try #require(model.metrics.first { $0.id == "primary" }) + #expect(primary.pacePercent == nil) + #expect(primary.detailLeftText == nil) + } + + /// Regression for the P1 review finding: `detailLeftText` is a shared slot, + /// so provider-owned text must survive when pace is hidden. + @Test + func `hiding pace keeps Kiro bonus credit text`() throws { + let now = Date() + let metadata = try #require(ProviderDefaults.metadata[.kiro]) + let snapshot = try UsageSnapshot( + primary: RateWindow( + usedPercent: 40, + windowMinutes: 300, + resetsAt: now.addingTimeInterval(3600), + resetDescription: nil), + secondary: RateWindow( + usedPercent: 30, + windowMinutes: 10080, + resetsAt: now.addingTimeInterval(86400), + resetDescription: nil), + details: [ProviderDetailSection(rows: [ + ProviderDetailSection.Row( + label: "Bonus credits left", + value: "500", + secondaryValue: "of 1000 · extra"), + ])], + updatedAt: now) + let model = UsageMenuCardView.Model.make(.init( + provider: .kiro, + metadata: metadata, + snapshot: snapshot, + credits: nil, + creditsError: nil, + dashboard: nil, + dashboardError: nil, + tokenSnapshot: nil, + tokenError: nil, + account: AccountInfo(email: nil, plan: nil), + isRefreshing: false, + lastError: nil, + usageBarsShowUsed: true, + resetTimeDisplayStyle: .countdown, + tokenCostUsageEnabled: false, + showOptionalCreditsAndExtraUsage: true, + hidePersonalInfo: false, + paceVisible: false, + now: now)) + + let credits = model.metrics.compactMap(\.detailLeftText).filter { $0.contains("bonus credits left") } + #expect(!credits.isEmpty) + } + + /// ZenMux without a reset date is the worst case the review flagged: the + /// shared detail slot carries that card's only reset information. + @Test + func `hiding pace keeps the ZenMux reset description`() throws { + let now = Date() + let metadata = try #require(ProviderDefaults.metadata[.zenmux]) + let snapshot = UsageSnapshot( + primary: RateWindow( + usedPercent: 40, + windowMinutes: 300, + resetsAt: now.addingTimeInterval(3600), + resetDescription: nil), + secondary: RateWindow( + usedPercent: 30, + windowMinutes: 10080, + resetsAt: nil, + resetDescription: "Credits do not reset"), + updatedAt: now) + let model = UsageMenuCardView.Model.make(.init( + provider: .zenmux, + metadata: metadata, + snapshot: snapshot, + credits: nil, + creditsError: nil, + dashboard: nil, + dashboardError: nil, + tokenSnapshot: nil, + tokenError: nil, + account: AccountInfo(email: nil, plan: nil), + isRefreshing: false, + lastError: nil, + usageBarsShowUsed: true, + resetTimeDisplayStyle: .countdown, + tokenCostUsageEnabled: false, + showOptionalCreditsAndExtraUsage: true, + hidePersonalInfo: false, + paceVisible: false, + now: now)) + + let details = model.metrics.compactMap(\.detailLeftText) + #expect(details.contains("Credits do not reset")) + } + + /// Quota and workday decorations are unrelated to pace and must survive. + @Test + func `hiding pace keeps quota warning markers`() throws { + let now = Date() + let metadata = try #require(ProviderDefaults.metadata[.claude]) + var input = Self.offPaceInput(now: now, metadata: metadata, paceVisible: false) + input = UsageMenuCardView.Model.Input( + provider: .claude, + metadata: metadata, + snapshot: input.snapshot, + credits: nil, + creditsError: nil, + dashboard: nil, + dashboardError: nil, + tokenSnapshot: nil, + tokenError: nil, + account: input.account, + isRefreshing: false, + lastError: nil, + usageBarsShowUsed: true, + resetTimeDisplayStyle: .countdown, + tokenCostUsageEnabled: false, + showOptionalCreditsAndExtraUsage: true, + hidePersonalInfo: false, + quotaWarningThresholds: [.session: [50, 20]], + paceVisible: false, + now: now) + let model = UsageMenuCardView.Model.make(input) + + let primary = try #require(model.metrics.first { $0.id == "primary" }) + #expect(primary.pacePercent == nil) + // usageBarsShowUsed: true mirrors thresholds, so 50/20 render at 50/80. + #expect(primary.warningMarkerPercents == [50, 80]) + } +} + +@MainActor +struct PaceVisibilitySettingsTests { + @Test + func `defaults pace to visible and seeds the raw key`() throws { + let suite = "SettingsStoreTests-pace-visible-defaults" + let defaults = try #require(UserDefaults(suiteName: suite)) + defaults.removePersistentDomain(forName: suite) + let configStore = testConfigStore(suiteName: suite) + let store = SettingsStore( + userDefaults: defaults, + configStore: configStore, + zaiTokenStore: NoopZaiTokenStore(), + syntheticTokenStore: NoopSyntheticTokenStore()) + + #expect(store.paceVisible == true) + #expect(defaults.object(forKey: "paceVisible") as? Bool == true) + + store.paceVisible = false + #expect(store.paceVisible == false) + #expect(defaults.object(forKey: "paceVisible") as? Bool == false) + } +} diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index 49637fdc03..fd16b9a2eb 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -884,13 +884,13 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact provider-owned construct passes a fixed identity to shared infrastructure."), SuppressedProviderReference( path: "Sources/CodexBar/SettingsStore+MenuObservation.swift", - line: 102, + line: 103, anchor: "_ = self[providerConfig: .synthetic, field: .apiKey]", expectedProviderIDs: ["synthetic"], reason: "This observation touchpoint reads a fixed provider field so UI invalidation tracks that setting."), SuppressedProviderReference( path: "Sources/CodexBar/SettingsStore+MenuObservation.swift", - line: 121, + line: 122, anchor: "_ = self[providerConfig: .warp, field: .apiKey]", expectedProviderIDs: ["warp"], reason: "This observation touchpoint reads a fixed provider field so UI invalidation tracks that setting."), @@ -1817,7 +1817,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift", - line: 208, + line: 227, anchor: "guard provider == .litellm,", expectedProviderIDs: ["litellm"], expectedReferenceCount: 1, @@ -1825,7 +1825,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift", - line: 251, + line: 301, anchor: "if input.provider == .kiro {", expectedProviderIDs: ["kilo", "kiro"], expectedReferenceCount: 2, @@ -1833,7 +1833,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift", - line: 269, + line: 319, anchor: "if input.provider == .mimo, input.snapshot != nil {", expectedProviderIDs: ["claude", "mimo", "opencodego"], expectedReferenceCount: 3, @@ -1841,7 +1841,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift", - line: 478, + line: 528, anchor: "if input.provider == .factory, snapshot.tertiary != nil {", expectedProviderIDs: ["alibabatokenplan", "amp", "crof", "cursor", "doubao", "factory", "grok", "sub2api"], expectedReferenceCount: 12, @@ -1862,7 +1862,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift", - line: 639, + line: 689, anchor: "case .minimax:", expectedProviderIDs: ["codex", "minimax", "poe"], expectedReferenceCount: 3, @@ -1870,7 +1870,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift", - line: 843, + line: 895, anchor: "if input.provider == .codex, !input.showOptionalCreditsAndExtraUsage {", expectedProviderIDs: ["claude", "codex", "copilot"], expectedReferenceCount: 4, @@ -1878,7 +1878,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift", - line: 868, + line: 920, anchor: "let resetText = input.provider == .sub2api && namedWindow.window.resetsAt == nil", expectedProviderIDs: ["doubao", "sub2api"], expectedReferenceCount: 3, @@ -1886,7 +1886,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift", - line: 939, + line: 992, anchor: "if input.provider == .antigravity,", expectedProviderIDs: ["antigravity"], expectedReferenceCount: 1, @@ -1894,7 +1894,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift", - line: 973, + line: 1026, anchor: "if provider == .claude, window.windowMinutes != 10080 {", expectedProviderIDs: ["antigravity", "claude", "codex"], expectedReferenceCount: 4, @@ -1902,7 +1902,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift", - line: 1005, + line: 1058, anchor: "guard input.provider == .antigravity else { return nil }", expectedProviderIDs: ["antigravity"], expectedReferenceCount: 1, @@ -1910,7 +1910,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 194, + line: 198, anchor: "if provider == .openrouter, metric.id == \"primary\" {", expectedProviderIDs: ["openrouter"], expectedReferenceCount: 1, @@ -1918,7 +1918,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 492, + line: 496, anchor: "if self.provider != .codex || self.showsCodexHint,", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -1926,7 +1926,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 674, + line: 678, anchor: "guard self.model.provider == .doubao else { return nil }", expectedProviderIDs: ["doubao"], expectedReferenceCount: 1, @@ -1934,7 +1934,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1055, + line: 1059, anchor: "if input.provider == .sub2api {", expectedProviderIDs: ["sub2api"], expectedReferenceCount: 1, @@ -1942,7 +1942,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "The sub2api menu card localizes and groups provider-owned usage detail rows for display."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1122, + line: 1126, anchor: "if provider == .kiro,", expectedProviderIDs: ["kilo", "kiro"], expectedReferenceCount: 2, @@ -1950,7 +1950,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1145, + line: 1149, anchor: "if provider == .minimax {", expectedProviderIDs: ["codex", "minimax"], expectedReferenceCount: 2, @@ -1958,7 +1958,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1180, + line: 1184, anchor: "guard let loginMethod = snapshot?.loginMethod(for: .kilo) else {", expectedProviderIDs: ["kilo"], expectedReferenceCount: 1, @@ -1966,7 +1966,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1264, + line: 1268, anchor: "if input.provider == .antigravity {", expectedProviderIDs: ["antigravity", "mistral"], expectedReferenceCount: 2, @@ -1974,7 +1974,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1284, + line: 1288, anchor: "if input.provider == .codex, let codexProjection = input.codexProjection {", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -1982,7 +1982,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1300, + line: 1304, anchor: "if input.provider != .codex, let weekly = snapshot.secondary {", expectedProviderIDs: ["alibaba", "alibabatokenplan", "codex", "perplexity", "sub2api"], expectedReferenceCount: 5, @@ -1996,7 +1996,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1339, + line: 1344, anchor: "if input.provider == .kilo || input.provider == .kimi,", expectedProviderIDs: ["kilo", "kimi"], expectedReferenceCount: 2, @@ -2004,7 +2004,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1435, + line: 1441, anchor: "var paceDetail = if input.provider == .kimi {", expectedProviderIDs: ["kimi"], expectedReferenceCount: 1, @@ -2012,7 +2012,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1451, + line: 1457, anchor: "if input.provider == .warp,", expectedProviderIDs: ["chutes", "kilo", "kiro", "litellm", "sub2api", "warp"], expectedReferenceCount: 6, @@ -2020,7 +2020,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1484, + line: 1490, anchor: "if input.provider == .alibaba || input.provider == .alibabatokenplan,", expectedProviderIDs: ["alibaba", "alibabatokenplan", "copilot", "crof", "manus", "perplexity", "zenmux"], expectedReferenceCount: 8, @@ -2037,7 +2037,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1525, + line: 1531, anchor: "if input.provider == .synthetic,", expectedProviderIDs: ["synthetic"], expectedReferenceCount: 1, @@ -2053,7 +2053,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuDescriptor.swift", - line: 444, + line: 449, anchor: "if provider == .kiro {", expectedProviderIDs: ["kiro"], expectedReferenceCount: 1, @@ -2061,7 +2061,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuDescriptor.swift", - line: 458, + line: 463, anchor: "} else if provider == .kilo {", expectedProviderIDs: ["kilo", "mimo", "openrouter", "poe"], expectedReferenceCount: 4, @@ -2069,7 +2069,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuDescriptor.swift", - line: 647, + line: 652, anchor: "let target = provider ?? store.enabledFirstPartyProviders().first ?? .codex", expectedProviderIDs: ["claude", "codex"], expectedReferenceCount: 4, @@ -2077,7 +2077,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuDescriptor.swift", - line: 675, + line: 680, anchor: "if provider == .factory, snapshot.tertiary != nil {", expectedProviderIDs: ["alibabatokenplan", "amp", "codex", "crof", "doubao", "factory", "grok", "sub2api"], expectedReferenceCount: 11, @@ -2097,7 +2097,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuDescriptor.swift", - line: 750, + line: 755, anchor: "let cleaned = if provider == .codex {", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -2281,7 +2281,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/SettingsStore.swift", - line: 1157, + line: 1163, anchor: "if !seen.contains(.factory), let zaiIndex = ordered.firstIndex(of: .zai) {", expectedProviderIDs: ["factory", "minimax", "zai"], expectedReferenceCount: 8,