From dfb87bdac0cb27c3e6fd092673a3482c032f2358 Mon Sep 17 00:00:00 2001 From: Jose Duarte Date: Fri, 12 Jun 2026 10:16:05 -0600 Subject: [PATCH 1/2] feat: show time until usage window reset in notch usage bars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The usage cache already stores resetsAt for both the five hour and seven day windows, and BuddyTabView already has a formatResetTime helper, but it was never wired into the UI. Append the countdown to the UsageBar detail label so the open notch shows e.g. "Session 3% · 4h 52m" and "Weekly 23% · Thu 8 PM". Co-Authored-By: Claude Fable 5 --- buddi/Buddi/Bridge/BuddyTabView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buddi/Buddi/Bridge/BuddyTabView.swift b/buddi/Buddi/Bridge/BuddyTabView.swift index ba000d1..ffa44e1 100644 --- a/buddi/Buddi/Bridge/BuddyTabView.swift +++ b/buddi/Buddi/Bridge/BuddyTabView.swift @@ -61,7 +61,7 @@ struct BuddyTabView: View { UsageBar( label: "Session", percent: fh.utilization / 100, - detail: "\(Int(fh.utilization))%", + detail: fh.resetsAt.map { "\(Int(fh.utilization))% · \(formatResetTime($0))" } ?? "\(Int(fh.utilization))%", color: fh.utilization > 80 ? .red : fh.utilization > 60 ? .yellow : Color(red: 0.35, green: 0.55, blue: 1.0) ) } @@ -69,7 +69,7 @@ struct BuddyTabView: View { UsageBar( label: "Weekly", percent: sd.utilization / 100, - detail: "\(Int(sd.utilization))%", + detail: sd.resetsAt.map { "\(Int(sd.utilization))% · \(formatResetTime($0))" } ?? "\(Int(sd.utilization))%", color: sd.utilization > 80 ? .red : sd.utilization > 60 ? .yellow : Color(red: 0.35, green: 0.55, blue: 1.0) ) } From f66cd56d2294151e797ca0f5b13a2da7f3e61197 Mon Sep 17 00:00:00 2001 From: Jose Duarte Date: Fri, 12 Jun 2026 11:13:09 -0600 Subject: [PATCH 2/2] fix: refresh reset countdown every minute via TimelineView The countdown string was computed at render time, so it only updated when UsageService published new data (5 min poll, up to 30 min with backoff). Drive the usage bars from a 60s TimelineView and compute the remaining time from context.date so the label stays current between polls. Extract the bars into a usageBars(now:) helper to keep layout identical (explicit VStack, spacing 3). Addresses cubic review feedback on the periodic refresh. Co-Authored-By: Claude Fable 5 --- buddi/Buddi/Bridge/BuddyTabView.swift | 43 ++++++++++++++++----------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/buddi/Buddi/Bridge/BuddyTabView.swift b/buddi/Buddi/Bridge/BuddyTabView.swift index ffa44e1..7bfd8ae 100644 --- a/buddi/Buddi/Bridge/BuddyTabView.swift +++ b/buddi/Buddi/Bridge/BuddyTabView.swift @@ -57,21 +57,10 @@ struct BuddyTabView: View { .foregroundColor(Color(nsColor: BuddyManager.shared.effectiveIdentity.rarity.nsColor).opacity(0.8)) if usageService.isAvailable { - if let fh = usageService.usage.fiveHour { - UsageBar( - label: "Session", - percent: fh.utilization / 100, - detail: fh.resetsAt.map { "\(Int(fh.utilization))% · \(formatResetTime($0))" } ?? "\(Int(fh.utilization))%", - color: fh.utilization > 80 ? .red : fh.utilization > 60 ? .yellow : Color(red: 0.35, green: 0.55, blue: 1.0) - ) - } - if let sd = usageService.usage.sevenDay { - UsageBar( - label: "Weekly", - percent: sd.utilization / 100, - detail: sd.resetsAt.map { "\(Int(sd.utilization))% · \(formatResetTime($0))" } ?? "\(Int(sd.utilization))%", - color: sd.utilization > 80 ? .red : sd.utilization > 60 ? .yellow : Color(red: 0.35, green: 0.55, blue: 1.0) - ) + TimelineView(.periodic(from: .now, by: 60)) { context in + VStack(spacing: 3) { + usageBars(now: context.date) + } } } } @@ -92,6 +81,26 @@ struct BuddyTabView: View { } } + @ViewBuilder + private func usageBars(now: Date) -> some View { + if let fh = usageService.usage.fiveHour { + UsageBar( + label: "Session", + percent: fh.utilization / 100, + detail: fh.resetsAt.map { "\(Int(fh.utilization))% · \(formatResetTime($0, now: now))" } ?? "\(Int(fh.utilization))%", + color: fh.utilization > 80 ? .red : fh.utilization > 60 ? .yellow : Color(red: 0.35, green: 0.55, blue: 1.0) + ) + } + if let sd = usageService.usage.sevenDay { + UsageBar( + label: "Weekly", + percent: sd.utilization / 100, + detail: sd.resetsAt.map { "\(Int(sd.utilization))% · \(formatResetTime($0, now: now))" } ?? "\(Int(sd.utilization))%", + color: sd.utilization > 80 ? .red : sd.utilization > 60 ? .yellow : Color(red: 0.35, green: 0.55, blue: 1.0) + ) + } + } + private var sessions: [SessionState] { bridge.sessionMonitor.instances } private static let dayTimeFormatter: DateFormatter = { @@ -100,8 +109,8 @@ struct BuddyTabView: View { return f }() - private func formatResetTime(_ date: Date) -> String { - let remaining = date.timeIntervalSinceNow + private func formatResetTime(_ date: Date, now: Date = Date()) -> String { + let remaining = date.timeIntervalSince(now) if remaining <= 0 { return "now" } let hours = Int(remaining) / 3600 let minutes = (Int(remaining) % 3600) / 60