Skip to content

Commit 0769b9e

Browse files
Alekstodosteipete
authored andcommitted
Add configurable quota warning notifications
1 parent 001561a commit 0769b9e

30 files changed

Lines changed: 1571 additions & 33 deletions

Sources/CodexBar/AppNotifications.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ final class AppNotifications {
1919
_ = self.ensureAuthorizationTask()
2020
}
2121

22-
func post(idPrefix: String, title: String, body: String, badge: NSNumber? = nil) {
22+
func post(
23+
idPrefix: String,
24+
title: String,
25+
body: String,
26+
badge: NSNumber? = nil,
27+
soundEnabled: Bool = true)
28+
{
2329
guard !Self.isRunningUnderTests else { return }
2430
let center = self.centerProvider()
2531
let logger = self.logger
@@ -34,7 +40,7 @@ final class AppNotifications {
3440
let content = UNMutableNotificationContent()
3541
content.title = title
3642
content.body = body
37-
content.sound = .default
43+
content.sound = soundEnabled ? .default : nil
3844
content.badge = badge
3945

4046
let request = UNNotificationRequest(
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import CodexBarCore
2+
3+
extension CodexConsumerProjection.RateLane {
4+
var quotaWarningWindow: QuotaWarningWindow {
5+
switch self {
6+
case .session:
7+
.session
8+
case .weekly:
9+
.weekly
10+
}
11+
}
12+
}
13+
14+
extension UsageMenuCardView.Model {
15+
static func warningMarkerPercents(thresholds: [Int]?, showUsed: Bool) -> [Double] {
16+
guard let thresholds, !thresholds.isEmpty else { return [] }
17+
return QuotaWarningThresholds.active(thresholds)
18+
.map { showUsed ? 100 - Double($0) : Double($0) }
19+
.filter { $0 > 0 && $0 < 100 }
20+
}
21+
}

Sources/CodexBar/MenuCardView.swift

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ struct UsageMenuCardView: View {
3636
let detailRightText: String?
3737
let pacePercent: Double?
3838
let paceOnTop: Bool
39+
let warningMarkerPercents: [Double]
3940

4041
init(
4142
id: String,
@@ -48,7 +49,8 @@ struct UsageMenuCardView: View {
4849
detailLeftText: String?,
4950
detailRightText: String?,
5051
pacePercent: Double?,
51-
paceOnTop: Bool)
52+
paceOnTop: Bool,
53+
warningMarkerPercents: [Double] = [])
5254
{
5355
self.id = id
5456
self.title = title
@@ -61,6 +63,7 @@ struct UsageMenuCardView: View {
6163
self.detailRightText = detailRightText
6264
self.pacePercent = pacePercent
6365
self.paceOnTop = paceOnTop
66+
self.warningMarkerPercents = warningMarkerPercents
6467
}
6568

6669
var percentLabel: String {
@@ -373,7 +376,8 @@ private struct MetricRow: View {
373376
tint: self.progressColor,
374377
accessibilityLabel: self.metric.percentStyle.accessibilityLabel,
375378
pacePercent: self.metric.pacePercent,
376-
paceOnTop: self.metric.paceOnTop)
379+
paceOnTop: self.metric.paceOnTop,
380+
warningMarkerPercents: self.metric.warningMarkerPercents)
377381
VStack(alignment: .leading, spacing: 2) {
378382
HStack(alignment: .firstTextBaseline) {
379383
Text(self.metric.percentLabel)
@@ -677,6 +681,7 @@ extension UsageMenuCardView.Model {
677681
let hidePersonalInfo: Bool
678682
let claudePeakHoursEnabled: Bool
679683
let weeklyPace: UsagePace?
684+
let quotaWarningThresholds: [QuotaWarningWindow: [Int]]
680685
let now: Date
681686

682687
init(
@@ -702,6 +707,7 @@ extension UsageMenuCardView.Model {
702707
hidePersonalInfo: Bool,
703708
claudePeakHoursEnabled: Bool = true,
704709
weeklyPace: UsagePace? = nil,
710+
quotaWarningThresholds: [QuotaWarningWindow: [Int]] = [:],
705711
now: Date)
706712
{
707713
self.provider = provider
@@ -726,6 +732,7 @@ extension UsageMenuCardView.Model {
726732
self.hidePersonalInfo = hidePersonalInfo
727733
self.claudePeakHoursEnabled = claudePeakHoursEnabled
728734
self.weeklyPace = weeklyPace
735+
self.quotaWarningThresholds = quotaWarningThresholds
729736
self.now = now
730737
}
731738
}
@@ -1010,7 +1017,10 @@ extension UsageMenuCardView.Model {
10101017
detailLeftText: nil,
10111018
detailRightText: nil,
10121019
pacePercent: nil,
1013-
paceOnTop: true))
1020+
paceOnTop: true,
1021+
warningMarkerPercents: Self.warningMarkerPercents(
1022+
thresholds: input.quotaWarningThresholds[.weekly],
1023+
showUsed: input.usageBarsShowUsed)))
10141024
}
10151025
if let extraRateWindows = snapshot.extraRateWindows {
10161026
metrics.append(contentsOf: extraRateWindows.map { namedWindow in
@@ -1175,7 +1185,10 @@ extension UsageMenuCardView.Model {
11751185
detailLeftText: primaryDetailLeft,
11761186
detailRightText: primaryDetailRight,
11771187
pacePercent: primaryPacePercent,
1178-
paceOnTop: primaryPaceOnTop)
1188+
paceOnTop: primaryPaceOnTop,
1189+
warningMarkerPercents: Self.warningMarkerPercents(
1190+
thresholds: input.quotaWarningThresholds[.session],
1191+
showUsed: input.usageBarsShowUsed))
11791192
}
11801193

11811194
private static func secondaryMetric(
@@ -1247,7 +1260,10 @@ extension UsageMenuCardView.Model {
12471260
detailLeftText: paceDetail?.leftLabel,
12481261
detailRightText: paceDetail?.rightLabel,
12491262
pacePercent: paceDetail?.pacePercent,
1250-
paceOnTop: paceDetail?.paceOnTop ?? true)
1263+
paceOnTop: paceDetail?.paceOnTop ?? true,
1264+
warningMarkerPercents: Self.warningMarkerPercents(
1265+
thresholds: input.quotaWarningThresholds[.weekly],
1266+
showUsed: input.usageBarsShowUsed))
12511267
}
12521268

12531269
private static func codexRateMetrics(
@@ -1286,7 +1302,10 @@ extension UsageMenuCardView.Model {
12861302
detailLeftText: paceDetail?.leftLabel,
12871303
detailRightText: paceDetail?.rightLabel,
12881304
pacePercent: paceDetail?.pacePercent,
1289-
paceOnTop: paceDetail?.paceOnTop ?? true)
1305+
paceOnTop: paceDetail?.paceOnTop ?? true,
1306+
warningMarkerPercents: Self.warningMarkerPercents(
1307+
thresholds: input.quotaWarningThresholds[lane.quotaWarningWindow],
1308+
showUsed: input.usageBarsShowUsed))
12901309
}
12911310
}
12921311

Sources/CodexBar/Notifications+CodexBar.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ extension Notification.Name {
66
static let codexbarDebugBlinkNow = Notification.Name("codexbarDebugBlinkNow")
77
static let codexbarWeeklyLimitReset = Notification.Name("codexbarWeeklyLimitReset")
88
static let codexbarProviderConfigDidChange = Notification.Name("codexbarProviderConfigDidChange")
9+
static let codexbarQuotaWarningDidPost = Notification.Name("codexbarQuotaWarningDidPost")
910
}
1011

1112
@MainActor
@@ -22,3 +23,18 @@ final class WeeklyLimitResetEvent: NSObject {
2223
self.usedPercent = usedPercent
2324
}
2425
}
26+
27+
@MainActor
28+
final class QuotaWarningPostedEvent: NSObject {
29+
let provider: UsageProvider
30+
let window: QuotaWarningWindow
31+
let threshold: Int
32+
let postedAt: Date
33+
34+
init(provider: UsageProvider, window: QuotaWarningWindow, threshold: Int, postedAt: Date) {
35+
self.provider = provider
36+
self.window = window
37+
self.threshold = threshold
38+
self.postedAt = postedAt
39+
}
40+
}

Sources/CodexBar/PreferencesGeneralPane.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ struct GeneralPane: View {
9696
subtitle: "Notifies when the 5-hour session quota hits 0% and when it becomes " +
9797
"available again.",
9898
binding: self.$settings.sessionQuotaNotificationsEnabled)
99+
PreferenceToggleRow(
100+
title: "Quota warning notifications",
101+
subtitle: "Warns when session or weekly quota remaining crosses configured thresholds.",
102+
binding: self.$settings.quotaWarningNotificationsEnabled)
103+
if self.settings.quotaWarningNotificationsEnabled {
104+
GlobalQuotaWarningSettingsView(settings: self.settings)
105+
}
99106
}
100107

101108
Divider()

Sources/CodexBar/PreferencesProviderDetailView.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ struct ProviderDetailView<SupplementaryContent: View>: View {
125125
self.supplementarySettingsContent
126126
}
127127

128+
ProviderQuotaWarningSettingsView(provider: self.provider, settings: self.store.settings)
129+
128130
if !self.settingsToggles.isEmpty {
129131
ProviderSettingsSection(title: "Options") {
130132
ForEach(self.settingsToggles) { toggle in
@@ -430,7 +432,8 @@ private struct ProviderMetricInlineRow: View {
430432
tint: self.progressColor,
431433
accessibilityLabel: self.metric.percentStyle.accessibilityLabel,
432434
pacePercent: self.metric.pacePercent,
433-
paceOnTop: self.metric.paceOnTop)
435+
paceOnTop: self.metric.paceOnTop,
436+
warningMarkerPercents: self.metric.warningMarkerPercents)
434437
.frame(minWidth: ProviderSettingsMetrics.metricBarWidth, maxWidth: .infinity)
435438

436439
HStack(alignment: .firstTextBaseline, spacing: 8) {

Sources/CodexBar/PreferencesProvidersPane.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,10 +605,19 @@ struct ProvidersPane: View {
605605
hidePersonalInfo: self.settings.hidePersonalInfo,
606606
claudePeakHoursEnabled: self.settings.claudePeakHoursEnabled,
607607
weeklyPace: weeklyPace,
608+
quotaWarningThresholds: [
609+
.session: self.quotaWarningMarkerThresholds(provider: provider, window: .session),
610+
.weekly: self.quotaWarningMarkerThresholds(provider: provider, window: .weekly),
611+
],
608612
now: now)
609613
return UsageMenuCardView.Model.make(input)
610614
}
611615

616+
private func quotaWarningMarkerThresholds(provider: UsageProvider, window: QuotaWarningWindow) -> [Int] {
617+
guard self.settings.quotaWarningEnabled(provider: provider, window: window) else { return [] }
618+
return self.settings.resolvedQuotaWarningThresholds(provider: provider, window: window)
619+
}
620+
612621
private func refreshCodexProvider() async {
613622
await ProviderInteractionContext.$current.withValue(.userInitiated) {
614623
await self.store.refreshCodexAccountScopedState(allowDisabled: true)

0 commit comments

Comments
 (0)