From 7a8caf4e07d692aa6e8b12ef21c10a43a0f0367b Mon Sep 17 00:00:00 2001 From: Steven Coaila Date: Sat, 26 Sep 2026 10:34:25 -0500 Subject: [PATCH 1/3] feat(stats): add Stats view with quota and 5-hour session forecasts Flip the panel to a Stats face scoped to the selected tab: weekly and 5-hour session forecasts from the server percentage, today against a usual day, 14-day chart, totals, streaks, models, effort, skills, turns and cache share. Cards collapse and remember it. Forecasts treat time on another Claude account as a marked gap, not pace or an early reset. The history records skill names and turn durations, and the cold scan checkpoints so it resumes. Panel controls move to SwiftUI, and in-panel clicks after a reopen no longer close the popover. --- AGENTS.md | 6 +- README.md | 10 +- .../App/StatusItemController.swift | 40 +- .../Resources/en.lproj/Localizable.strings | 76 ++- .../Resources/es.lproj/Localizable.strings | 76 ++- .../Services/AppPreferences.swift | 6 + .../TokenGaugeApp/Services/StatsModel.swift | 114 ++++ .../TokenGaugeApp/Services/UsageStore.swift | 12 + .../TokenGaugeApp/Support/ModelActivity.swift | 24 +- Sources/TokenGaugeApp/Support/StatsCard.swift | 13 + .../TokenGaugeApp/Support/StatsSummary.swift | 286 +++++++++ .../Support/UsageFormatters.swift | 9 + .../Views/CompactQuotaWindowRow.swift | 4 +- Sources/TokenGaugeApp/Views/FlipCard.swift | 80 +++ .../Views/ForecastChartView.swift | 360 +++++++++++ .../Views/HistoryModeControl.swift | 134 +++- .../Views/HistoryPanelView.swift | 75 +-- .../Views/HistoryPeriodControl.swift | 73 --- Sources/TokenGaugeApp/Views/PopoverView.swift | 53 +- .../TokenGaugeApp/Views/ProviderCard.swift | 25 +- .../TokenGaugeApp/Views/QuotaPaceLabel.swift | 43 ++ Sources/TokenGaugeApp/Views/QuotaRing.swift | 18 +- .../TokenGaugeApp/Views/QuotaWindowRow.swift | 35 +- .../TokenGaugeApp/Views/StatsBarsView.swift | 134 ++++ Sources/TokenGaugeApp/Views/StatsView.swift | 580 ++++++++++++++++++ Sources/TokenGaugeCapture/main.swift | 8 +- .../TokenGaugeCore/ActivityUsageRecord.swift | 84 +++ .../TokenGaugeCore/ActivityUsageScanner.swift | 89 +++ .../TokenGaugeCore/ClaudeHistoryScanner.swift | 2 +- .../EffortTranscriptMetadata.swift | 58 ++ .../TokenGaugeCore/EffortUsageRecord.swift | 22 +- .../TokenGaugeCore/EffortUsageScanner.swift | 5 +- Sources/TokenGaugeCore/QuotaForecast.swift | 201 ++++++ Sources/TokenGaugeCore/ScanState.swift | 85 ++- .../UsageHistoryStore+Effort.swift | 77 ++- .../UsageHistoryStore+Schema.swift | 17 + .../TokenGaugeCore/UsageHistoryStore.swift | 22 +- .../AppPreferencesTests.swift | 13 + .../ModelActivityTests.swift | 6 +- .../PopoverLayoutTests.swift | 28 + .../ReadmeScreenshotTests.swift | 98 +++ .../StatsSummaryTests.swift | 75 +++ .../WindowVisibilityTests.swift | 20 +- .../ActivityUsageScannerTests.swift | 260 ++++++++ .../ObservedHistoryTotalsTests.swift | 8 +- .../QuotaForecastTests.swift | 136 ++++ .../TranscriptScannerTests.swift | 11 +- docs/images/stats.png | Bin 0 -> 274472 bytes scripts/usage_history.sh | 7 +- 49 files changed, 3383 insertions(+), 235 deletions(-) create mode 100644 Sources/TokenGaugeApp/Services/StatsModel.swift create mode 100644 Sources/TokenGaugeApp/Support/StatsCard.swift create mode 100644 Sources/TokenGaugeApp/Support/StatsSummary.swift create mode 100644 Sources/TokenGaugeApp/Views/FlipCard.swift create mode 100644 Sources/TokenGaugeApp/Views/ForecastChartView.swift delete mode 100644 Sources/TokenGaugeApp/Views/HistoryPeriodControl.swift create mode 100644 Sources/TokenGaugeApp/Views/StatsBarsView.swift create mode 100644 Sources/TokenGaugeApp/Views/StatsView.swift create mode 100644 Sources/TokenGaugeCore/ActivityUsageRecord.swift create mode 100644 Sources/TokenGaugeCore/ActivityUsageScanner.swift create mode 100644 Sources/TokenGaugeCore/QuotaForecast.swift create mode 100644 Tests/TokenGaugeAppTests/StatsSummaryTests.swift create mode 100644 Tests/TokenGaugeCoreTests/ActivityUsageScannerTests.swift create mode 100644 Tests/TokenGaugeCoreTests/QuotaForecastTests.swift create mode 100644 docs/images/stats.png diff --git a/AGENTS.md b/AGENTS.md index 6a06b2e..38521df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,11 +32,12 @@ - Every successful refresh archives into the local SQLite history at `~/Library/Application Support/TokenGauge/usage-history.sqlite` (mode 0600) through `UsageHistoryStore`, off the main actor and best-effort: an archive failure must never block or alter the panel. `quota_samples` keeps percentages and reset instants collapsed into 15-minute buckets and pruned after `quotaRetentionDays`; `daily_tokens` keeps one row per (day, provider, model) and only ever grows (`MAX`), so trimmed transcripts cannot erase recorded history. Codex account totals use the model key `all`; local effort events can supply separate model detail. Never store tokens, account fields, prompts, or responses there. - Archive quota only from successful original provider results with a capture timestamp, never from mixed UI fallbacks. Archive tokens independently after a successful activity read. Preserve explicit zero days; missing days remain unknown. Never fill missing Claude days from an empty scan. - Effort scans run in the bounded `TokenGaugeCapture --record-effort-history` subprocess, never the resident UI. Decode only allowlisted metadata; hash response IDs before storage. Deduplicate events using MAX counters and preserve known model/effort values. The daily total is MAX(provider aggregate, summed model rows, summed effort events), never a sum of overlapping sources. Effort events and daily totals have no expiry. +- The same scan records `activity_events`: Claude `Skill` tool names and Codex skills read through a plain `sed|cat|head|nl|less|bat|tail` of `skills//SKILL.md` (name validated against `[a-z0-9][a-z0-9._:-]{0,63}`, counted once per message/rollout), plus root turn durations (Claude `turn_duration`, Codex `task_complete`; subagents and sidechains excluded). `effort_events.cached_tokens` keeps cache reads. Never store tool arguments, commands, paths or messages. Claude SDK sessions write no `turn_duration`, so turn tiles hide when today has none. The scan checkpoints its state every 5 s so a timed-out cold scan resumes instead of restarting. - Weekly pace requires at least three verified, same-reset observations spanning 30 minutes within the last hour, with a latest reading no older than 20 minutes. Reset, decrease, invalid reading, or a gap over 30 minutes breaks continuity. Reset timestamps may vary by at most one second against a persisted continuity anchor; larger changes break continuity. Legacy quota rows remain unverified. Archive valid estimates in `pace_samples` without expiry, including idle readings; only newly increased usage updates the last active pace. A current pace must match both the latest verified quota timestamp in SQLite and the visible snapshot capture timestamp. Show retained pace as historical with its measurement date, never as a fresh reading. Never attribute quota pace to a reasoning effort from token counts. - Calendar reset markers group coincident weekly quotas per provider. The next live date is confirmed; later weeks project the same seven-day interval for a year and are labelled expected. Only ready, visible quotas contribute; cancellation or unavailable data removes the forecast. Future reset cards omit activity and streaks. - History queries run off the main actor and load only the selected date range; hover and selection never query SQLite or scan logs. Calendar weeks begin Monday. Unknown days must not become zero-use days. Preview/test stores must never read or write the real user history. - History hover updates the selected detail only on actual pointer movement; layout/scroll/refresh must not select the date beneath a stationary cursor. The annual view centers today on entry and on Today, preserves the viewport during refresh, and highlights today independently of selection. The native calendar positions its canvas synchronously before drawing. Verify real hover, opening, past-date selection, Today and refresh; suppress gutters with `.scrollIndicators(.never)` in SwiftUI and native scroller flags in the calendar. -- Query the history with `scripts/usage_history.sh {status|daily|models|weekly|monthly|quota|export|sql}`; the `daily_totals`, `weekly_totals` (ISO week Monday) and `monthly_totals` views exist so both Steven and an agent read the same aggregation. +- Query the history with `scripts/usage_history.sh {status|daily|models|weekly|monthly|quota|skills|export|sql}`; the `daily_totals`, `weekly_totals` (ISO week Monday) and `monthly_totals` views exist so both Steven and an agent read the same aggregation. ## Architecture @@ -48,7 +49,8 @@ - Never leave `popover.animates = false` in steady state: AppKit closes a `.transient` popover on the mouse-down of the status item click, before `togglePopover` runs, so a parked `false` makes that close instant. Disable animation only around the silent re-anchor `show()` and restore it in the same turn. - No continuous menu bar or hidden-popover animations. The seven-day chart uses one or two native bars per day according to display mode and per-day hover/click targets; hidden providers never affect its legend, summary or scale; never rebuild its data for every cursor pixel. Skip status-item image/title assignment when unchanged. - Panel style and menu-bar style persist independently; existing installs default to classic panels and numeric indicators. Compact/ring styles retain exact quota and reset data, historical provenance, and provider/window selection. Graphics never show stale values as live; exact menu values remain in tooltip/accessibility. Animations are brief, event-driven, optional, and honor Reduce Motion. -- Use the native segmented control for history modes so selection and labels share one renderer. Year/week transitions must be checked in actual pixels after parent resizing; aligned accessibility frames alone cannot prove that the selection background stayed attached. +- Panel controls are SwiftUI (`HistoryModeControl`, `HistoryPeriodControl`); keep AppKit controls out of the popover. `popoverShouldClose` refuses a close whose current event is a mouse-down inside the panel window unless the app requested it (`requestClose`); the `popover` log category (debug) records every close with its stack. Year/week transitions must be checked in actual pixels after parent resizing. +- Stats is the back face of the panel (`FlipCard`, finite spring flip, back face pinned to the front height so the popover never resizes mid-flip). It is scoped to the selected tab. Forecasts (weekly and 5 h session, `QuotaForecast`) project the server percentage, never tokens: weekly pace sums observed drops over 72 h and 24 h across resets, session pace covers the session so far and its last hour; time on another Claude account is a marked gap that is neither pace nor an early reset (only same-account upward jumps are); the budget is the remaining share per day or per hour left. Every Stats animation is finite and honours `quotaAnimationsEnabled` and Reduce Motion. - Ring grids must cap columns at the number of visible windows and distribute the occupied row across the card. Two quotas must never leave a reserved third slot; cover this with a wide two-cell render check. Unified ring width follows visible quotas up to 604 pt, enough for one Codex ring and three Claude rings in a single row. - Centralize visual constants in `Theme.swift`; individual panels are 340 pt wide and Unified is 560 pt wide; the panel grows to fit its content, with the layout matrix verified below 700 pt. Never wrap the entire popover in a vertical scroll view; only unusually long provider lists scroll within their own bounded area. Individual views show only their own provider; Unified shows two titled cards side by side. Scroll only when real provider content exceeds its bounded height. - The popover closes on any click outside it: `.transient` alone does not dismiss an accessory app's popover when the click lands in another application (Steven, 2026-08-27). `StatusItemController` arms a global mouse-down monitor plus `didResignActiveNotification` while the popover is shown and tears both down in `popoverDidClose`. Keep the monitor to MOUSE events only — a global key monitor would demand Accessibility, which this app must never request. diff --git a/README.md b/README.md index 5a4d7ab..9f04d3d 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,18 @@ MIT License

-TokenGauge lives in your Mac’s menu bar and shows how much Claude Code or Codex quota you have left. Check reset times, browse weekly and yearly activity, and keep a private local history without opening a terminal. +TokenGauge lives in your Mac’s menu bar and shows how much Claude Code or Codex quota you have left. Check reset times, see whether your weekly limit and 5-hour session will last, browse weekly and yearly activity, and keep a private local history without opening a terminal.

Annual activity calendar with weekly reset forecasts, Codex quota, and all three Claude quota rings

App views rendered with synthetic demonstration data.

+
+Stats: forecasts, today, models, effort, and skills +

Stats view with weekly and 5-hour forecasts, today against a usual day, the last 14 days, top models, reasoning effort, and most used skills

+
+
Classic panels, yearly history, and Settings

Weekly reset forecasts and streaks above the local activity calendar

@@ -29,6 +34,7 @@ TokenGauge lives in your Mac’s menu bar and shows how much Claude Code or Code ## At a glance +- **Stats and forecasts:** flip the panel to see whether each weekly limit and the current 5-hour session will last until its reset, projected from the provider’s own percentage. Stats also compares today with a usual day, charts the last 14 days, and shows streaks, top models, reasoning effort, most used skills, turns, and cache share. Time on another Claude account is marked instead of being mistaken for a reset. Collapse any card you do not need. - **Three views:** Codex, Claude, or Unified. Individual views show only that provider; Unified puts both quota cards side by side. - **Flexible layouts:** keep the classic panel, choose compact rows, or use separate quota rings. Switch from the panel header or Settings → Panel. - **A readable menu bar:** show one or several limits per provider (5-hour, weekly, model weekly), hide either provider, and choose percentages, mini bars, or mini rings with Large, Medium, or Small sizing. Settings previews your real menu bar as you click, and right-clicking the menu bar item changes the same options instantly. Exact percentages remain available on hover and in the panel. @@ -74,7 +80,7 @@ If a live read fails, historical balances are not presented as current quota. Au Codex metrics come from the local `codex app-server` account methods. Claude quota comes from its read-only usage endpoint, using the existing credential only in memory. This integration depends on provider behavior and is not an official provider product. -Local activity scans decode timestamps, message IDs, model identifiers, reasoning effort, and numeric counters. Prompt and response fields are not decoded, logged, or persisted. Only normalized metrics and aggregate history are saved under `~/Library/Application Support/TokenGauge`, with user-only permissions. No usage is sent to a TokenGauge server. Provider quota requests contact the provider, and update checks contact GitHub. +Local activity scans decode timestamps, message IDs, model identifiers, reasoning effort, skill names, turn durations, and numeric counters. Prompt and response fields are not decoded, logged, or persisted. Only normalized metrics and aggregate history are saved under `~/Library/Application Support/TokenGauge`, with user-only permissions. No usage is sent to a TokenGauge server. Provider quota requests contact the provider, and update checks contact GitHub. See [SECURITY.md](SECURITY.md) for reporting and update-channel details. diff --git a/Sources/TokenGaugeApp/App/StatusItemController.swift b/Sources/TokenGaugeApp/App/StatusItemController.swift index 5e9a737..2824fd5 100644 --- a/Sources/TokenGaugeApp/App/StatusItemController.swift +++ b/Sources/TokenGaugeApp/App/StatusItemController.swift @@ -2,6 +2,7 @@ import AppKit import Combine import SwiftUI import TokenGaugeCore +import os @MainActor final class StatusItemController: NSObject { @@ -19,6 +20,8 @@ final class StatusItemController: NSObject { private var history: HistoryDashboardModel? private var previousApp: NSRunningApplication? private weak var trackingMenu: NSMenu? + private var closeRequested = false + private static let log = Logger(subsystem: "com.stevenacz.TokenGauge", category: "popover") private struct PopoverGeometry: Equatable { let sourceWindow: NSRect @@ -107,7 +110,7 @@ final class StatusItemController: NSObject { return } if popover.isShown { - popover.performClose(nil) + requestClose() return } store.refresh() @@ -171,8 +174,14 @@ final class StatusItemController: NSObject { outsideClickMonitor = NSEvent.addGlobalMonitorForEvents( matching: [.leftMouseDown, .rightMouseDown, .otherMouseDown] ) { [weak self] _ in + let location = NSEvent.mouseLocation Task { @MainActor in guard let self, self.trackingMenu == nil else { return } + guard self.popover.contentViewController?.view.window?.frame.contains(location) != true else { + Self.log.debug("outside monitor ignored a click inside the panel") + return + } + Self.log.debug("outside monitor closes") self.dismissPopover() } } @@ -183,7 +192,10 @@ final class StatusItemController: NSObject { object: nil, queue: .main ) { [weak self] _ in - Task { @MainActor in self?.dismissPopover() } + Task { @MainActor in + Self.log.debug("resign closes") + self?.dismissPopover() + } } } } @@ -204,7 +216,20 @@ final class StatusItemController: NSObject { stopDismissMonitors() return } + requestClose() + } + + private func requestClose() { + closeRequested = true popover.performClose(nil) + closeRequested = false + } + + private func isPanelClick(_ event: NSEvent?) -> Bool { + guard let event, [.leftMouseDown, .rightMouseDown, .otherMouseDown].contains(event.type), + let panel = popover.contentViewController?.view.window + else { return false } + return event.window === panel } private func updateStatusItem() { @@ -330,10 +355,19 @@ extension StatusItemController { extension StatusItemController: NSPopoverDelegate { func popoverShouldClose(_ popover: NSPopover) -> Bool { + let event = NSApp.currentEvent + if !closeRequested, isPanelClick(event) { + Self.log.debug("close refused: click inside panel") + return false + } guard trackingMenu != nil, let window = statusItem.button?.window else { return true } - return NSApp.currentEvent?.window !== window + return event?.window !== window } func popoverWillClose(_ notification: Notification) { + let event = NSApp.currentEvent + Self.log.debug( + "will close requested=\(self.closeRequested) event=\(event.map { String($0.type.rawValue) } ?? "none", privacy: .public) window=\(event?.window.map { String(describing: type(of: $0)) } ?? "none", privacy: .public)" + ) trackingMenu?.cancelTrackingWithoutAnimation() } func popoverDidClose(_ notification: Notification) { diff --git a/Sources/TokenGaugeApp/Resources/en.lproj/Localizable.strings b/Sources/TokenGaugeApp/Resources/en.lproj/Localizable.strings index e427af0..24202d8 100644 --- a/Sources/TokenGaugeApp/Resources/en.lproj/Localizable.strings +++ b/Sources/TokenGaugeApp/Resources/en.lproj/Localizable.strings @@ -164,7 +164,7 @@ "pace.details" = "Estimated percentage points of weekly allowance per hour, over %@ observed minutes in the last hour. This account-wide rate is not attributed to a reasoning effort."; "pace.collecting" = "Pace: —"; "pace.insufficient" = "Requires at least three verified readings covering 30 minutes. Resets and long gaps are excluded."; -"pace.setting" = "Show weekly use per hour"; +"pace.setting" = "Show pace and session forecast"; "history.all_efforts" = "All efforts"; "history.heading" = "Tokens"; "history.details" = "Model & effort"; @@ -210,3 +210,77 @@ "settings.right_click.show" = "Show me"; "settings.panel" = "Panel"; "settings.accounts" = "Accounts"; + +"stats.open" = "Show stats"; +"stats.close" = "Back to panel"; +"stats.loading" = "Crunching…"; +"stats.load_failed" = "Stats unavailable"; +"stats.card.collapse" = "Hide"; +"stats.card.expand" = "Show"; +"stats.card.collapsed" = "Hidden"; +"stats.card.expanded" = "Shown"; +"stats.forecast.title" = "Will it last until the reset?"; +"stats.forecast.unavailable" = "No active limit to project."; +"stats.forecast.help" = "The dashed line projects what you used over the last 3 days and the dotted one your last 24 h. It uses the server's real percentage, not tokens. «Max» is what you can use per day to reach the reset. Time spent on another account does not count toward the pace."; +"stats.headline.lasts" = "You'll make it"; +"stats.headline.tight" = "Cutting it close"; +"stats.headline.runs_out" = "Won't last"; +"stats.headline.exhausted" = "Used up"; +"stats.headline.no_usage" = "No usage yet"; +"stats.verdict.lasts" = "At this pace you reach the reset with ~%@ left."; +"stats.verdict.tight" = "You'll make it, with only ~%@ left."; +"stats.verdict.runs_out" = "Runs out %1$@, %2$@ before the reset."; +"stats.verdict.exhausted" = "No quota left until the reset."; +"stats.verdict.no_usage" = "No usage in this window yet."; +"stats.row.left" = "%@ left"; +"stats.reset_in" = "Resets %@"; +"stats.pace.average" = "3 d: %@/day"; +"stats.pace.today" = "24 h: %@/day"; +"stats.budget" = "Max %@/day"; +"session.detail_runs_out" = "At this session's pace (%1$@/h) it runs out %2$@ before the reset."; +"session.detail" = "At this session's pace (%1$@/h) you reach the reset with ~%2$@ left."; +"session.runs_out" = "Runs out at %@"; +"session.tight" = "Cutting it close · %@/h"; +"session.lasts" = "You'll make it · %@/h"; +"stats.verdict.runs_out_session" = "Runs out at %1$@, %2$@ before the reset."; +"stats.budget.hour" = "Max %@/h"; +"stats.pace.hour" = "1 h: %@/h"; +"stats.pace.session" = "Session: %@/h"; +"stats.forecast.help_session" = "The dashed line projects your pace so far this session and the dotted one your last hour. It uses the server's real percentage, not tokens. «Max» is what you can use per hour to reach the reset. Time spent on another account does not count toward the pace."; +"stats.chart.now" = "Now · %@"; +"stats.chart.reset" = "Reset"; +"stats.chart.expected" = "est. "; +"stats.today.title" = "Today"; +"stats.today.typical" = "Usual day %@"; +"stats.today.ratio" = "%@× your usual day"; +"stats.insight.new_model" = "You're trying %@: it shows in your usage."; +"stats.insight.spike" = "You're well above your usual day."; +"stats.insight.quiet" = "Quiet day: below your usual usage."; +"stats.insight.steady" = "Today's usage is within your normal range."; +"stats.week" = "This week"; +"stats.month" = "This month"; +"stats.lifetime" = "Total"; +"stats.since" = "since %@"; +"stats.peak" = "Best day"; +"stats.streak" = "Streak"; +"stats.streak_best" = "best %@ d"; +"stats.days_value" = "%@ d"; +"stats.active_days" = "Active days"; +"stats.vs_previous" = "Compared with the same point of the previous period"; +"stats.daily.title" = "Last 14 days"; +"stats.daily.typical" = "usual day"; +"stats.other" = "Other"; +"stats.activity.title" = "Activity"; +"stats.activity.today" = "Worked today"; +"stats.activity.turns" = "%@ turns"; +"stats.activity.longest" = "Longest turn"; +"stats.activity.longest_hint" = "last 30 days"; +"stats.activity.cache" = "From cache"; +"stats.activity.cache_hint" = "of all tokens · 7 d"; +"stats.models.title" = "Models · 7 days"; +"stats.efforts.title" = "Effort · 7 days"; +"stats.skills.title" = "Top skills · 30 days"; +"stats.skills.runs" = "%@ runs"; +"stats.skills.runs_one" = "%@ run"; +"stats.chart.away" = "Other account · %1$@ → %2$@"; +"stats.chart.boost" = "Early reset: the quota went back up before the scheduled date (a provider reset or a banked reset)."; diff --git a/Sources/TokenGaugeApp/Resources/es.lproj/Localizable.strings b/Sources/TokenGaugeApp/Resources/es.lproj/Localizable.strings index cf96c81..a26fb63 100644 --- a/Sources/TokenGaugeApp/Resources/es.lproj/Localizable.strings +++ b/Sources/TokenGaugeApp/Resources/es.lproj/Localizable.strings @@ -164,7 +164,7 @@ "pace.details" = "Puntos porcentuales estimados de la cuota semanal por hora, con %@ minutos observados en la última hora. Este ritmo corresponde a la cuenta, no a un nivel de esfuerzo concreto."; "pace.collecting" = "Ritmo: —"; "pace.insufficient" = "Requiere al menos tres lecturas verificadas que cubran 30 minutos. Se excluyen reinicios y huecos largos."; -"pace.setting" = "Mostrar uso semanal por hora"; +"pace.setting" = "Mostrar ritmo y pronóstico de sesión"; "history.all_efforts" = "Todos los esfuerzos"; "history.heading" = "Tokens"; "history.details" = "Modelo y esfuerzo"; @@ -210,3 +210,77 @@ "settings.right_click.show" = "Ver cómo"; "settings.panel" = "Panel"; "settings.accounts" = "Cuentas"; + +"stats.open" = "Ver estadísticas"; +"stats.close" = "Volver al panel"; +"stats.loading" = "Calculando…"; +"stats.load_failed" = "Estadísticas no disponibles"; +"stats.card.collapse" = "Ocultar"; +"stats.card.expand" = "Mostrar"; +"stats.card.collapsed" = "Oculta"; +"stats.card.expanded" = "Visible"; +"stats.forecast.title" = "¿Te alcanza hasta el reinicio?"; +"stats.forecast.unavailable" = "No hay un límite activo para proyectar."; +"stats.forecast.help" = "La línea discontinua proyecta lo que gastaste en los últimos 3 días y la punteada, lo de las últimas 24 h. Usa el porcentaje real del servidor, no tokens. «Máx.» es lo que puedes gastar al día para llegar al reinicio. El tiempo que pasas en otra cuenta no cuenta para el ritmo."; +"stats.headline.lasts" = "Te alcanza"; +"stats.headline.tight" = "Muy justo"; +"stats.headline.runs_out" = "No te alcanza"; +"stats.headline.exhausted" = "Agotado"; +"stats.headline.no_usage" = "Sin consumo aún"; +"stats.verdict.lasts" = "A este ritmo llegas al reinicio con ~%@ libre."; +"stats.verdict.tight" = "Llegas, pero con apenas ~%@ libre."; +"stats.verdict.runs_out" = "Se acaba el %1$@, %2$@ antes del reinicio."; +"stats.verdict.exhausted" = "Ya no queda cupo hasta el reinicio."; +"stats.verdict.no_usage" = "Todavía no hay consumo en esta ventana."; +"stats.row.left" = "sobra %@"; +"stats.reset_in" = "Reinicia %@"; +"stats.pace.average" = "3 d: %@/día"; +"stats.pace.today" = "24 h: %@/día"; +"stats.budget" = "Máx. %@/día"; +"session.detail_runs_out" = "A tu ritmo de esta sesión (%1$@/h) se agota %2$@ antes del reinicio."; +"session.detail" = "A tu ritmo de esta sesión (%1$@/h) llegas al reinicio con ~%2$@ libre."; +"session.runs_out" = "Se agota a las %@"; +"session.tight" = "Muy justo · %@/h"; +"session.lasts" = "Te alcanza · %@/h"; +"stats.verdict.runs_out_session" = "Se acaba a las %1$@, %2$@ antes del reinicio."; +"stats.budget.hour" = "Máx. %@/h"; +"stats.pace.hour" = "1 h: %@/h"; +"stats.pace.session" = "Sesión: %@/h"; +"stats.forecast.help_session" = "La línea discontinua proyecta tu ritmo en lo que va de la sesión y la punteada, el de la última hora. Usa el porcentaje real del servidor, no tokens. «Máx.» es lo que puedes gastar por hora para llegar al reinicio. El tiempo que pasas en otra cuenta no cuenta para el ritmo."; +"stats.chart.now" = "Ahora · %@"; +"stats.chart.reset" = "Reinicio"; +"stats.chart.expected" = "prev. "; +"stats.today.title" = "Hoy"; +"stats.today.typical" = "Día normal %@"; +"stats.today.ratio" = "%@× tu día normal"; +"stats.insight.new_model" = "Estrenas %@: se nota en tu consumo."; +"stats.insight.spike" = "Hoy vas muy por encima de tu día normal."; +"stats.insight.quiet" = "Día tranquilo: vas por debajo de lo normal."; +"stats.insight.steady" = "Tu consumo de hoy está dentro de lo normal."; +"stats.week" = "Esta semana"; +"stats.month" = "Este mes"; +"stats.lifetime" = "Total"; +"stats.since" = "desde %@"; +"stats.peak" = "Mejor día"; +"stats.streak" = "Racha"; +"stats.streak_best" = "mejor %@ d"; +"stats.days_value" = "%@ d"; +"stats.active_days" = "Días activos"; +"stats.vs_previous" = "Frente al mismo punto del periodo anterior"; +"stats.daily.title" = "Últimos 14 días"; +"stats.daily.typical" = "día normal"; +"stats.other" = "Otros"; +"stats.activity.title" = "Actividad"; +"stats.activity.today" = "Trabajo hoy"; +"stats.activity.turns" = "%@ turnos"; +"stats.activity.longest" = "Turno más largo"; +"stats.activity.longest_hint" = "últimos 30 días"; +"stats.activity.cache" = "Desde caché"; +"stats.activity.cache_hint" = "del total · 7 d"; +"stats.models.title" = "Modelos · 7 días"; +"stats.efforts.title" = "Esfuerzo · 7 días"; +"stats.skills.title" = "Skills más usadas · 30 días"; +"stats.skills.runs" = "%@ usos"; +"stats.skills.runs_one" = "%@ uso"; +"stats.chart.away" = "Otra cuenta · %1$@ → %2$@"; +"stats.chart.boost" = "Reinicio anticipado: el cupo subió antes de la fecha programada (lo reinició el proveedor o usaste un reinicio guardado)."; diff --git a/Sources/TokenGaugeApp/Services/AppPreferences.swift b/Sources/TokenGaugeApp/Services/AppPreferences.swift index 2872eb1..0b30605 100644 --- a/Sources/TokenGaugeApp/Services/AppPreferences.swift +++ b/Sources/TokenGaugeApp/Services/AppPreferences.swift @@ -13,6 +13,7 @@ struct AppPreferences { static let showHourlyPace = "showHourlyPace" static let showLunaReserve = "showLunaReserve" static let hiddenClaudeWindows = "hiddenClaudeWindows" + static let collapsedStatsCards = "collapsedStatsCards" static let claudeMenuBarSource = "claudeMenuBarSource" static let claudeMenuBarWindows = "claudeMenuBarWindows" static let codexMenuBarWindows = "codexMenuBarWindows" @@ -92,6 +93,11 @@ struct AppPreferences { nonmutating set { defaults.set(newValue.map(\.rawValue).sorted(), forKey: Key.hiddenClaudeWindows) } } + var collapsedStatsCards: Set { + get { StatsCard.decode(defaults.stringArray(forKey: Key.collapsedStatsCards)) } + nonmutating set { defaults.set(newValue.map(\.rawValue).sorted(), forKey: Key.collapsedStatsCards) } + } + var claudeMenuBarWindows: Set { get { if let stored = defaults.stringArray(forKey: Key.claudeMenuBarWindows) { diff --git a/Sources/TokenGaugeApp/Services/StatsModel.swift b/Sources/TokenGaugeApp/Services/StatsModel.swift new file mode 100644 index 0000000..835d64f --- /dev/null +++ b/Sources/TokenGaugeApp/Services/StatsModel.swift @@ -0,0 +1,114 @@ +import Combine +import Foundation +import TokenGaugeCore + +@MainActor +final class StatsModel: ObservableObject { + struct Records: Sendable { + var input: StatsSummary.Input + var quota: [HistoryQuotaRow] + } + + @Published private(set) var records: Records? + @Published private(set) var loadFailed = false + @Published private(set) var isLoading = false + private var loadedRevision: Int? + private var summaries: [String: StatsSummary] = [:] + private let historyURL: URL + private let preview: Records? + + nonisolated static let quotaLookback: TimeInterval = 8 * 86_400 + + init(preview: Records? = nil, historyURL: URL = UsagePaths.history()) { + self.preview = preview + self.historyURL = historyURL + records = preview + } + + func summary(for providers: [UsageProvider], codexSummary: UsageSummary?) -> StatsSummary? { + guard var records else { return nil } + let key = providers.map(\.rawValue).joined(separator: "+") + if let cached = summaries[key] { return cached } + records.input.codexSummary = codexSummary + let value = StatsSummary.make(records.input, providers: providers) + summaries[key] = value + return value + } + + func forecasts(for states: [(UsageProvider, [QuotaWindow])], accountFingerprint: String?, now: Date = Date()) + -> [QuotaForecast] + { + let rows = records?.quota ?? [] + return states.flatMap { provider, windows in + windows.filter { [300, 10_080].contains($0.durationMinutes) }.compactMap { window in + QuotaForecast.make( + provider: provider, window: window, rows: rows, now: now, + accountFingerprint: provider == .claude ? accountFingerprint : nil) + } + } + } + + func load(revision: Int) async { + guard preview == nil, loadedRevision != revision || records == nil else { return } + isLoading = true + let url = historyURL + let now = Date() + do { + let loaded = try await Task.detached(priority: .userInitiated) { + try Self.read(url: url, now: now) + }.value + guard !Task.isCancelled else { return } + summaries.removeAll() + records = loaded + loadedRevision = revision + loadFailed = false + } catch { + guard !Task.isCancelled else { return } + loadFailed = records == nil + } + isLoading = false + } + + static func preview(snapshots: [ProviderUsageSnapshot]) -> Records { + let rows = snapshots.flatMap { snapshot in + snapshot.dailyUsage.map { + HistoryTokenRow(day: $0.day, provider: snapshot.provider, model: "all", tokens: $0.tokens) + } + } + let today = Calendar.current.startOfDay(for: Date()) + let start = Calendar.current.date(byAdding: .day, value: -(StatsSummary.recentDays - 1), to: today) ?? today + let interval = DateInterval( + start: start, end: Calendar.current.date(byAdding: .day, value: 1, to: today) ?? today) + return Records( + input: StatsSummary.Input( + days: HistoryDashboardModel.makeDays(interval: interval, tokens: rows, efforts: []), tokens: rows, + efforts: []), + quota: []) + } + + nonisolated static func read(url: URL, now: Date) throws -> Records { + let calendar = Calendar.current + let today = calendar.startOfDay(for: now) + let tokens = try UsageHistoryStore.tokenRows(at: url) + let efforts = try UsageHistoryStore.effortRows(at: url) + let firstKey = try UsageHistoryStore.bounds(at: url).firstDay + let recentStart = calendar.date(byAdding: .day, value: -(StatsSummary.recentDays - 1), to: today) ?? today + let first = firstKey.flatMap(date(from:)).map { min($0, recentStart) } ?? recentStart + let interval = DateInterval(start: first, end: calendar.date(byAdding: .day, value: 1, to: today) ?? now) + let since = HistoryDashboardModel.dayKey( + calendar.date(byAdding: .day, value: -StatsSummary.skillDays, to: today) ?? today) + return Records( + input: StatsSummary.Input( + days: HistoryDashboardModel.makeDays(interval: interval, tokens: tokens, efforts: efforts), + tokens: tokens, efforts: efforts, + activity: (try? UsageHistoryStore.activityRows(since: since, at: url)) ?? [], + cached: (try? UsageHistoryStore.cachedTokenRows(since: since, at: url)) ?? []), + quota: try UsageHistoryStore.quotaRows(since: now.addingTimeInterval(-quotaLookback), until: now, at: url)) + } + + nonisolated private static func date(from key: String) -> Date? { + let parts = key.split(separator: "-").compactMap { Int($0) } + guard parts.count == 3 else { return nil } + return Calendar.current.date(from: DateComponents(year: parts[0], month: parts[1], day: parts[2])) + } +} diff --git a/Sources/TokenGaugeApp/Services/UsageStore.swift b/Sources/TokenGaugeApp/Services/UsageStore.swift index 1e0ba4d..f3a7db4 100644 --- a/Sources/TokenGaugeApp/Services/UsageStore.swift +++ b/Sources/TokenGaugeApp/Services/UsageStore.swift @@ -67,6 +67,9 @@ final class UsageStore: ObservableObject { @Published private(set) var hiddenClaudeWindows: Set { didSet { preferences.hiddenClaudeWindows = hiddenClaudeWindows } } + @Published private(set) var collapsedStatsCards: Set { + didSet { preferences.collapsedStatsCards = collapsedStatsCards } + } @Published var claudeMenuBarWindows: Set { didSet { preferences.claudeMenuBarWindows = claudeMenuBarWindows } } @@ -153,6 +156,7 @@ final class UsageStore: ObservableObject { animateChanges = preferences.animateChanges showLunaReserve = preferences.showLunaReserve hiddenClaudeWindows = preferences.hiddenClaudeWindows + collapsedStatsCards = preferences.collapsedStatsCards claudeMenuBarWindows = preferences.claudeMenuBarWindows codexMenuBarWindows = preferences.codexMenuBarWindows hideAccountLabel = preferences.hideAccountLabel @@ -411,6 +415,14 @@ final class UsageStore: ObservableObject { hiddenClaudeWindows = hidden } + func toggleStatsCard(_ card: StatsCard) { + if collapsedStatsCards.contains(card) { + collapsedStatsCards.remove(card) + } else { + collapsedStatsCards.insert(card) + } + } + func setCancelled(_ cancelled: Bool, for provider: UsageProvider) { guard isCancelled(provider: provider) != cancelled else { return } persistCancellation(cancelled ? Date() : nil, for: provider) diff --git a/Sources/TokenGaugeApp/Support/ModelActivity.swift b/Sources/TokenGaugeApp/Support/ModelActivity.swift index f3e47a1..e7e4826 100644 --- a/Sources/TokenGaugeApp/Support/ModelActivity.swift +++ b/Sources/TokenGaugeApp/Support/ModelActivity.swift @@ -20,14 +20,14 @@ enum ModelActivity { ) -> [ModelUsageChip] { let scoped = family.map { name in - buckets.filter { displayName($0.model).caseInsensitiveCompare(name) == .orderedSame } - } ?? buckets.filter { !excluded.contains(displayName($0.model).lowercased()) } + buckets.filter { Self.family($0.model).caseInsensitiveCompare(name) == .orderedSame } + } ?? buckets.filter { !excluded.contains(Self.family($0.model).lowercased()) } return ModelTokenAggregator.byModel(scoped, since: since) .prefix(limit) .map { ModelUsageChip(model: $0.model, tokens: $0.tokens) } } - static func displayName(_ raw: String) -> String { + static func family(_ raw: String) -> String { let identifier = raw.lowercased() for (needle, label) in knownFamilies where identifier.contains(needle) { return label @@ -36,6 +36,24 @@ enum ModelActivity { return raw } + static func displayName(_ raw: String) -> String { + let label = family(raw) + let identifier = String(raw.lowercased().prefix { $0 != "[" }) + if identifier.hasPrefix("gpt-") { + let parts = identifier.split(separator: "-") + guard parts.count > 1 else { return raw } + let words = parts.dropFirst(2).map { $0.prefix(1).uppercased() + $0.dropFirst() } + return (["GPT-" + parts[1]] + words).joined(separator: " ") + } + guard let needle = knownFamilies.first(where: { identifier.contains($0.0) })?.0, + let range = identifier.range(of: needle + "-") + else { return label } + let version = identifier[range.upperBound...].split(separator: "-").prefix { part in + part.count <= 2 && part.allSatisfy(\.isNumber) + } + return version.isEmpty ? label : label + " " + version.joined(separator: ".") + } + private static let knownFamilies: [(String, String)] = [ ("fable", "Fable"), ("opus", "Opus"), diff --git a/Sources/TokenGaugeApp/Support/StatsCard.swift b/Sources/TokenGaugeApp/Support/StatsCard.swift new file mode 100644 index 0000000..e0adec4 --- /dev/null +++ b/Sources/TokenGaugeApp/Support/StatsCard.swift @@ -0,0 +1,13 @@ +enum StatsCard: String, CaseIterable { + case forecast + case today + case daily + case activity + case models + case efforts + case skills + + static func decode(_ values: [String]?) -> Set { + Set((values ?? []).compactMap(StatsCard.init(rawValue:))) + } +} diff --git a/Sources/TokenGaugeApp/Support/StatsSummary.swift b/Sources/TokenGaugeApp/Support/StatsSummary.swift new file mode 100644 index 0000000..0ea9c0e --- /dev/null +++ b/Sources/TokenGaugeApp/Support/StatsSummary.swift @@ -0,0 +1,286 @@ +import Foundation +import TokenGaugeCore + +struct StatsSegment: Identifiable, Equatable, Sendable { + let id: String + let tokens: Int + let provider: UsageProvider + let rank: Int +} + +struct StatsDayTotal: Identifiable, Equatable, Sendable { + let id: String + let date: Date + let total: Int? + let segments: [StatsSegment] +} + +struct StatsRankItem: Identifiable, Equatable, Sendable { + let id: String + let label: String + let value: Int + let provider: UsageProvider? +} + +struct StatsPeak: Equatable, Sendable { + let date: Date + let tokens: Int +} + +struct StatsTurns: Equatable, Sendable { + let todayMilliseconds: Int + let todayCount: Int + let longestMilliseconds: Int +} + +enum StatsInsight: Equatable, Sendable { + case newModel(String) + case spike(Double) + case quiet(Double) + case steady(Double) +} + +struct StatsSummary: Equatable, Sendable { + var today: Int? + var typicalDay: Int? + var week = 0 + var previousWeek: Int? + var month = 0 + var previousMonth: Int? + var lifetime = 0 + var since: Date? + var peak: StatsPeak? + var activeDays = 0 + var streak = HistoryStreak() + var recent: [StatsDayTotal] = [] + var legend: [StatsSegment] = [] + var models: [StatsRankItem] = [] + var efforts: [StatsRankItem] = [] + var skills: [StatsRankItem] = [] + var turns: StatsTurns? + var cachedShare: Double? + var insight: StatsInsight? + + static let recentDays = 14 + static let typicalWindow = 28 + static let rankingDays = 7 + static let skillDays = 30 + + var ratio: Double? { + guard let today, let typicalDay, typicalDay > 0 else { return nil } + return Double(today) / Double(typicalDay) + } + + struct Input: Sendable { + var days: [HistoryCalendarDay] + var tokens: [HistoryTokenRow] + var efforts: [HistoryEffortRow] + var activity: [HistoryActivityRow] = [] + var cached: [HistoryCachedRow] = [] + var codexSummary: UsageSummary? + } + + @MainActor static func make( + _ input: Input, providers: [UsageProvider], now: Date = Date(), calendar base: Calendar = .current + ) -> StatsSummary { + var calendar = base + calendar.firstWeekday = 2 + calendar.minimumDaysInFirstWeek = 4 + let today = calendar.startOfDay(for: now) + let todayKey = HistoryDashboardModel.dayKey(today) + let days = input.days.filter { $0.date <= today } + let totals = Dictionary(uniqueKeysWithValues: days.map { ($0.id, $0.total(for: providers)) }) + func key(_ offset: Int, from date: Date = today) -> String { + HistoryDashboardModel.dayKey(calendar.date(byAdding: .day, value: offset, to: date) ?? date) + } + func sum(_ keys: [String]) -> Int? { + let values = keys.compactMap { totals[$0] ?? nil } + return values.isEmpty ? nil : values.reduce(0, &+) + } + + var summary = StatsSummary() + summary.today = totals[todayKey] ?? nil + + let previous = (1...typicalWindow).compactMap { totals[key(-$0)] ?? nil }.filter { $0 > 0 }.sorted() + if previous.count >= 3 { summary.typicalDay = previous[previous.count / 2] } + + let weekStart = calendar.dateInterval(of: .weekOfYear, for: today)?.start ?? today + let weekElapsed = (calendar.dateComponents([.day], from: weekStart, to: today).day ?? 0) + 1 + summary.week = sum((0.. 0 + { + summary.peak = StatsPeak(date: best.date, tokens: tokens) + } + if providers.contains(.codex), let codex = input.codexSummary { + let local = days.compactMap { $0.tokens(for: .codex) }.reduce(0, &+) + if let lifetime = codex.lifetimeTokens, lifetime > local { summary.lifetime += lifetime - local } + if providers == [.codex], let peak = codex.peakDailyTokens, peak > (summary.peak?.tokens ?? 0) { + summary.peak = StatsPeak(date: summary.peak?.date ?? today, tokens: peak) + } + } + let usageDays = Set(recorded.filter { ($0.total(for: providers) ?? 0) > 0 }.map(\.id)) + summary.activeDays = usageDays.count + let streak = HistoryAnalytics.streaks(usageDays: usageDays, today: todayKey, calendar: calendar) + summary.streak = HistoryStreak(current: streak.current, longest: streak.longest) + + let modelRows = modelTokens(input, providers: providers) + let rankingKeys = Set((0.. 1 + ? recentKeys.flatMap { day in + providers.compactMap { provider in + days.first { $0.id == day }?.tokens(for: provider).map { + LabeledTokens(day: day, label: provider.rawValue, provider: provider, tokens: $0) + } + } + } + : modelRows.filter { recentSet.contains($0.day) } + let leaders = rank(segmentRows, limit: providers.count > 1 ? 2 : 3) + let leaderIndex = Dictionary(uniqueKeysWithValues: leaders.enumerated().map { ($1.id, $0) }) + let fallback = providers.first ?? .claude + summary.legend = leaders.enumerated().map { index, item in + StatsSegment(id: item.id, tokens: item.value, provider: item.provider ?? fallback, rank: index) + } + summary.recent = zip(recentKeys, recentDates).map { day, date in + let total = totals[day] ?? nil + var buckets: [String: Int] = [:] + for row in segmentRows where row.day == day { + buckets[leaderIndex[row.label] != nil ? row.label : otherLabel, default: 0] += row.tokens + } + let assigned = buckets.values.reduce(0, &+) + if let total, total > assigned { buckets[otherLabel, default: 0] += total - assigned } + let segments = buckets.filter { $0.value > 0 }.map { label, tokens in + StatsSegment( + id: label, tokens: tokens, provider: leaders.first { $0.id == label }?.provider ?? fallback, + rank: leaderIndex[label] ?? leaders.count) + }.sorted { $0.rank < $1.rank } + return StatsDayTotal(id: day, date: date, total: total, segments: segments) + } + + let skillKeys = Set((0.. 0 { + summary.cachedShare = Double(cached.map(\.cachedTokens).reduce(0, &+)) / Double(cachedTotal) + } + + summary.insight = insight( + summary, modelRows: modelRows, todayKey: todayKey, firstDay: recorded.first?.id, now: now, + calendar: calendar) + return summary + } + + static let otherLabel = "other" + + private struct LabeledTokens { + let day: String + let label: String + let provider: UsageProvider? + let tokens: Int + } + + @MainActor private static func modelTokens(_ input: Input, providers: [UsageProvider]) -> [LabeledTokens] { + var rows: [LabeledTokens] = [] + if providers.contains(.claude) { + rows += input.tokens.filter { $0.provider == .claude && $0.model != "all" && $0.tokens > 0 }.map { + LabeledTokens(day: $0.day, label: $0.model, provider: .claude, tokens: $0.tokens) + } + } + if providers.contains(.codex) { + rows += input.efforts.filter { $0.provider == .codex && $0.tokens > 0 }.map { + LabeledTokens(day: $0.day, label: $0.model, provider: .codex, tokens: $0.tokens) + } + } + return rows.map { + LabeledTokens(day: $0.day, label: modelLabel($0.label), provider: $0.provider, tokens: $0.tokens) + } + } + + private static func rank(_ rows: [LabeledTokens], limit: Int) -> [StatsRankItem] { + var totals: [String: (Int, UsageProvider?)] = [:] + for row in rows { + let current = totals[row.label] + totals[row.label] = ((current?.0 ?? 0) &+ row.tokens, current?.1 ?? row.provider) + } + return totals.map { StatsRankItem(id: $0.key, label: $0.key, value: $0.value.0, provider: $0.value.1) } + .filter { $0.value > 0 } + .sorted { $0.value == $1.value ? $0.label < $1.label : $0.value > $1.value } + .prefix(limit).map { $0 } + } + + private static func insight( + _ summary: StatsSummary, modelRows: [LabeledTokens], todayKey: String, firstDay: String?, now: Date, + calendar: Calendar + ) -> StatsInsight? { + let firstSeen = Dictionary(grouping: modelRows, by: \.label).compactMapValues { $0.map(\.day).min() } + let recentCutoff = HistoryDashboardModel.dayKey(calendar.date(byAdding: .day, value: -2, to: now) ?? now) + let historyCutoff = HistoryDashboardModel.dayKey(calendar.date(byAdding: .day, value: -9, to: now) ?? now) + if let firstDay, firstDay <= historyCutoff, + let newest = firstSeen.filter({ $0.value >= recentCutoff && $0.key != otherLabel }) + .max(by: { $0.value == $1.value ? $0.key > $1.key : $0.value < $1.value }) + { + return .newModel(newest.key) + } + guard let ratio = summary.ratio else { return nil } + if ratio >= 1.8 { return .spike(ratio) } + if ratio <= 0.4, calendar.component(.hour, from: now) >= 15 { return .quiet(ratio) } + return .steady(ratio) + } + + @MainActor private static func modelLabel(_ raw: String) -> String { + ModelActivity.displayName(raw) + } + + @MainActor static func effortLabel(_ raw: String) -> String { + switch raw { + case "unknown": return "history.effort_unknown".localized + case "xhigh": return "XHigh" + default: return raw.capitalized + } + } +} diff --git a/Sources/TokenGaugeApp/Support/UsageFormatters.swift b/Sources/TokenGaugeApp/Support/UsageFormatters.swift index 5dde254..dda511b 100644 --- a/Sources/TokenGaugeApp/Support/UsageFormatters.swift +++ b/Sources/TokenGaugeApp/Support/UsageFormatters.swift @@ -10,6 +10,15 @@ enum UsageFormatters { "\(Int(value.rounded()))%" } + static func duration(_ seconds: TimeInterval) -> String { + let minutes = Int((seconds / 60).rounded()) + if minutes < 1 { return "\(Int(seconds.rounded())) s" } + if minutes < 60 { return "\(minutes) min" } + let hours = minutes / 60 + if hours < 48 { return minutes % 60 == 0 ? "\(hours) h" : "\(hours) h \(minutes % 60) min" } + return "\(hours / 24) d \(hours % 24) h" + } + static func tokens(_ value: Int) -> String { if value >= 1_000_000_000 { return "\(decimal(Double(value) / 1_000_000_000))B" diff --git a/Sources/TokenGaugeApp/Views/CompactQuotaWindowRow.swift b/Sources/TokenGaugeApp/Views/CompactQuotaWindowRow.swift index 2561d26..122cb11 100644 --- a/Sources/TokenGaugeApp/Views/CompactQuotaWindowRow.swift +++ b/Sources/TokenGaugeApp/Views/CompactQuotaWindowRow.swift @@ -9,6 +9,7 @@ struct CompactQuotaWindowRow: View { var showPace = false var pace: QuotaPace? var previousPace: QuotaPace? + var session: QuotaForecast? var body: some View { HStack(alignment: .center, spacing: 14) { @@ -37,6 +38,7 @@ struct CompactQuotaWindowRow: View { .font(.system(size: 9)) .foregroundStyle(.secondary) if showPace { QuotaPaceLabel(pace: pace, previousPace: previousPace) } + if let session { SessionForecastLabel(forecast: session) } } .fixedSize(horizontal: true, vertical: false) } @@ -44,7 +46,7 @@ struct CompactQuotaWindowRow: View { [UsageFormatters.windowHelp(window), UsageFormatters.modelChips(chips)].compactMap { $0 }.joined( separator: "\n") ) - .accessibilityElement(children: showPace ? .contain : .combine) + .accessibilityElement(children: showPace || session != nil ? .contain : .combine) .accessibilityHint(UsageFormatters.modelChips(chips) ?? "") } } diff --git a/Sources/TokenGaugeApp/Views/FlipCard.swift b/Sources/TokenGaugeApp/Views/FlipCard.swift new file mode 100644 index 0000000..48c4cb2 --- /dev/null +++ b/Sources/TokenGaugeApp/Views/FlipCard.swift @@ -0,0 +1,80 @@ +import SwiftUI + +struct FlipCard: View { + let flipped: Bool + let animated: Bool + @ViewBuilder let front: () -> Front + @ViewBuilder let back: () -> Back + @State private var angle: Double = 0 + @State private var target = false + @State private var showsFront = true + @State private var showsBack = false + @State private var frontHeight: CGFloat = 0 + + static var minimumBackHeight: CGFloat { 360 } + + var body: some View { + ZStack(alignment: .top) { + if showsFront { + front() + .fixedSize(horizontal: false, vertical: true) + .onGeometryChange(for: CGFloat.self) { + $0.size.height + } action: { height in + if !showsBack { frontHeight = height } + } + .modifier(FlipFace(angle: angle, isBack: false)) + } + if showsBack { + back() + .frame(height: max(frontHeight, Self.minimumBackHeight)) + .modifier(FlipFace(angle: angle, isBack: true)) + } + } + .onChange(of: flipped) { _, newValue in turn(to: newValue) } + } + + private func turn(to value: Bool) { + target = value + showsFront = true + showsBack = true + let destination: Double = value ? 180 : 0 + guard animated else { + angle = destination + settle(value) + return + } + withAnimation(.spring(response: 0.52, dampingFraction: 0.84), completionCriteria: .logicallyComplete) { + angle = destination + } completion: { + settle(value) + } + } + + private func settle(_ value: Bool) { + guard value == target else { return } + showsFront = !value + showsBack = value + } +} + +private struct FlipFace: ViewModifier, Animatable { + var angle: Double + let isBack: Bool + + nonisolated var animatableData: Double { + get { angle } + set { angle = newValue } + } + + func body(content: Content) -> some View { + let visible = isBack ? angle > 90 : angle <= 90 + let depth = 1 - 0.07 * sin(angle * .pi / 180) + content + .rotation3DEffect(.degrees(isBack ? angle - 180 : angle), axis: (0, 1, 0), perspective: 0.32) + .scaleEffect(depth) + .opacity(visible ? 1 : 0) + .allowsHitTesting(visible && (angle == 0 || angle == 180)) + .accessibilityHidden(!visible) + } +} diff --git a/Sources/TokenGaugeApp/Views/ForecastChartView.swift b/Sources/TokenGaugeApp/Views/ForecastChartView.swift new file mode 100644 index 0000000..1470f48 --- /dev/null +++ b/Sources/TokenGaugeApp/Views/ForecastChartView.swift @@ -0,0 +1,360 @@ +import SwiftUI +import TokenGaugeCore + +struct ForecastChartView: View { + let forecast: QuotaForecast + let tint: Color + let locale: Locale + @Environment(\.accessibilityReduceMotion) private var reduceMotion + @Environment(\.quotaAnimationsEnabled) private var animateChanges + @State private var drawn: CGFloat = 0 + @State private var projected: CGFloat = 0 + @State private var hoverDate: Date? + + static let height: CGFloat = 138 + private let inset = EdgeInsets(top: 14, leading: 30, bottom: 18, trailing: 6) + + private var animates: Bool { animateChanges && !reduceMotion } + private var drawProgress: CGFloat { animates ? drawn : 1 } + private var projectionProgress: CGFloat { animates ? projected : 1 } + private var danger: Bool { forecast.runsOutAt != nil || forecast.remaining <= 0 } + private var projectionTint: Color { danger ? .red : tint } + + var body: some View { + GeometryReader { proxy in + let plot = CGRect( + x: inset.leading, y: inset.top, width: max(1, proxy.size.width - inset.leading - inset.trailing), + height: max(1, proxy.size.height - inset.top - inset.bottom)) + ZStack(alignment: .topLeading) { + grid(plot) + awayBands(plot) + area(plot) + .fill( + LinearGradient( + colors: [tint.opacity(0.28), tint.opacity(0.02)], startPoint: .top, endPoint: .bottom) + ) + .opacity(Double(drawProgress)) + line(plot).trim(from: 0, to: drawProgress) + .stroke(tint, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round)) + if let recent = recentProjection(plot) { + recent.trim(from: 0, to: projectionProgress) + .stroke( + Color.secondary.opacity(0.55), + style: StrokeStyle(lineWidth: 1.2, lineCap: .round, dash: [1.5, 3.5])) + } + projection(plot).trim(from: 0, to: projectionProgress) + .stroke( + projectionTint.opacity(0.9), style: StrokeStyle(lineWidth: 1.6, lineCap: .round, dash: [5, 4])) + markers(plot) + if let hoverDate { hoverOverlay(plot, date: hoverDate) } + } + .contentShape(Rectangle()) + .onContinuousHover { phase in + switch phase { + case .active(let location): + let fraction = min(max((location.x - plot.minX) / plot.width, 0), 1) + hoverDate = forecast.start.addingTimeInterval(Double(fraction) * span) + case .ended: + hoverDate = nil + } + } + } + .frame(height: Self.height) + .onAppear(perform: play) + .accessibilityElement(children: .ignore) + .accessibilityLabel(accessibilitySummary) + } + + private var span: TimeInterval { max(1, forecast.reset.timeIntervalSince(forecast.start)) } + + private func x(_ date: Date, _ plot: CGRect) -> CGFloat { + plot.minX + CGFloat(min(max(date.timeIntervalSince(forecast.start) / span, 0), 1)) * plot.width + } + + private func y(_ remaining: Double, _ plot: CGRect) -> CGFloat { + plot.maxY - CGFloat(min(max(remaining, 0), 100) / 100) * plot.height + } + + private func play() { + guard animates else { + drawn = 1 + projected = 1 + return + } + drawn = 0 + projected = 0 + withAnimation(.easeOut(duration: 0.75)) { drawn = 1 } + withAnimation(.easeInOut(duration: 0.5).delay(0.6)) { projected = 1 } + } + + private var segments: [[ForecastPoint]] { + let starts = Set(forecast.boosts.map(\.date)) + var result: [[ForecastPoint]] = [] + for point in forecast.points { + if let last = result.last?.last, + !starts.contains(point.date) && !QuotaForecast.crosses(forecast.away, last, point) + { + result[result.count - 1].append(point) + } else { + result.append([point]) + } + } + return result + } + + private func awayInterval(at date: Date) -> DateInterval? { + forecast.away.first { $0.start <= date && date <= $0.end && $0.end > forecast.start } + } + + private func awayLabel(_ interval: DateInterval) -> String { + let left = forecast.points.last { $0.date <= interval.start }?.remaining + let back = forecast.points.first { $0.date >= interval.end }?.remaining + return "stats.chart.away".localized( + left.map(UsageFormatters.percentage) ?? "—", back.map(UsageFormatters.percentage) ?? "—") + } + + private func awayBands(_ plot: CGRect) -> some View { + ZStack(alignment: .topLeading) { + ForEach(forecast.away.filter { $0.end > forecast.start }, id: \.start) { interval in + let minX = x(interval.start, plot) + let width = x(interval.end, plot) - minX + RoundedRectangle(cornerRadius: 3) + .fill(Color.primary.opacity(0.06)) + .frame(width: max(1, width), height: plot.height) + .position(x: minX + width / 2, y: plot.midY) + if width >= 16 { + Image(systemName: "person.2.fill") + .font(.system(size: 8)) + .foregroundStyle(.tertiary) + .position(x: minX + width / 2, y: plot.minY + 9) + .accessibilityHidden(true) + } + } + } + .opacity(Double(drawProgress)) + .allowsHitTesting(false) + } + + private func line(_ plot: CGRect) -> Path { + Path { path in + for segment in segments { + for (index, point) in segment.enumerated() { + let location = CGPoint(x: x(point.date, plot), y: y(point.remaining, plot)) + if index == 0 { path.move(to: location) } else { path.addLine(to: location) } + } + } + } + } + + private func area(_ plot: CGRect) -> Path { + Path { path in + for segment in segments { + guard let first = segment.first, let last = segment.last else { continue } + path.move(to: CGPoint(x: x(first.date, plot), y: plot.maxY)) + for point in segment { + path.addLine(to: CGPoint(x: x(point.date, plot), y: y(point.remaining, plot))) + } + path.addLine(to: CGPoint(x: x(last.date, plot), y: plot.maxY)) + path.closeSubpath() + } + } + } + + private func projectionPath(pace: Double?, runsOut: Date?, _ plot: CGRect) -> Path? { + guard let pace, forecast.remaining > 0 else { return nil } + let start = CGPoint(x: x(forecast.now, plot), y: y(forecast.remaining, plot)) + return Path { path in + path.move(to: start) + if let runsOut { + path.addLine(to: CGPoint(x: x(runsOut, plot), y: plot.maxY)) + path.addLine(to: CGPoint(x: plot.maxX, y: plot.maxY)) + } else { + path.addLine( + to: CGPoint(x: plot.maxX, y: y(forecast.remaining - pace * forecast.hoursLeft, plot))) + } + } + } + + private func projection(_ plot: CGRect) -> Path { + projectionPath(pace: forecast.pointsPerHour, runsOut: forecast.runsOutAt, plot) ?? Path() + } + + private func recentProjection(_ plot: CGRect) -> Path? { + guard let recent = forecast.recentPointsPerHour, let average = forecast.pointsPerHour, + abs(recent - average) * forecast.hoursLeft >= 5 + else { return nil } + return projectionPath(pace: recent, runsOut: forecast.recentRunsOutAt, plot) + } + + private func grid(_ plot: CGRect) -> some View { + ZStack(alignment: .topLeading) { + ForEach([0.0, 50.0, 100.0], id: \.self) { value in + Path { path in + path.move(to: CGPoint(x: plot.minX, y: y(value, plot))) + path.addLine(to: CGPoint(x: plot.maxX, y: y(value, plot))) + } + .stroke(Color.primary.opacity(value == 0 ? 0.14 : 0.07), lineWidth: 1) + Text("\(Int(value))%") + .font(.system(size: 8, weight: .medium)).monospacedDigit() + .foregroundStyle(.tertiary) + .frame(width: plot.minX - 5, alignment: .trailing) + .position(x: (plot.minX - 5) / 2, y: y(value, plot)) + } + ForEach(forecast.isSession ? hourTicks(plot) : dayTicks(plot), id: \.date) { tick in + Text(tick.label) + .font(.system(size: 8, weight: tick.isCurrent ? .semibold : .regular)) + .foregroundStyle(tick.isCurrent ? AnyShapeStyle(.primary) : AnyShapeStyle(.tertiary)) + .position(x: tick.x, y: plot.maxY + 10) + } + } + } + + private struct AxisTick { + let date: Date + let x: CGFloat + let label: String + let isCurrent: Bool + } + + private func hourTicks(_ plot: CGRect) -> [AxisTick] { + let calendar = Calendar.current + let current = calendar.dateInterval(of: .hour, for: forecast.now)?.start + var hour = calendar.dateInterval(of: .hour, for: forecast.start)?.end ?? forecast.reset + var ticks: [AxisTick] = [] + while hour < forecast.reset { + if hour.timeIntervalSince(forecast.start) >= 600, forecast.reset.timeIntervalSince(hour) >= 600 { + ticks.append( + AxisTick( + date: hour, x: x(hour, plot), label: hour.formatted(.dateTime.hour().locale(locale)), + isCurrent: hour == current)) + } + hour = calendar.date(byAdding: .hour, value: 1, to: hour) ?? forecast.reset + } + return ticks + } + + private func dayTicks(_ plot: CGRect) -> [AxisTick] { + let calendar = Calendar.current + var ticks: [AxisTick] = [] + var day = calendar.startOfDay(for: forecast.start) + let today = calendar.startOfDay(for: forecast.now) + while day < forecast.reset { + let next = calendar.date(byAdding: .day, value: 1, to: day) ?? forecast.reset + let middle = Date( + timeIntervalSince1970: (max(day, forecast.start).timeIntervalSince1970 + + min(next, forecast.reset).timeIntervalSince1970) / 2) + if min(next, forecast.reset).timeIntervalSince(max(day, forecast.start)) >= 6 * 3600 { + ticks.append( + AxisTick( + date: day, x: x(middle, plot), + label: day.formatted(.dateTime.weekday(.narrow).locale(locale)), + isCurrent: day == today)) + } + day = next + } + return ticks + } + + private func markers(_ plot: CGRect) -> some View { + let nowX = x(forecast.now, plot) + let nowY = y(forecast.remaining, plot) + return ZStack(alignment: .topLeading) { + Path { path in + path.move(to: CGPoint(x: nowX, y: plot.minY - 6)) + path.addLine(to: CGPoint(x: nowX, y: plot.maxY)) + } + .stroke(Color.primary.opacity(0.25), style: StrokeStyle(lineWidth: 1, dash: [2, 3])) + Path { path in + path.move(to: CGPoint(x: plot.maxX, y: plot.minY - 6)) + path.addLine(to: CGPoint(x: plot.maxX, y: plot.maxY)) + } + .stroke(tint.opacity(0.45), style: StrokeStyle(lineWidth: 1, dash: [2, 3])) + Text("stats.chart.now".localized(UsageFormatters.percentage(forecast.remaining))) + .font(.system(size: 8, weight: .semibold)).monospacedDigit() + .foregroundStyle(.secondary) + .fixedSize() + .position(x: min(max(nowX, plot.minX + 26), plot.maxX - 60), y: plot.minY - 8) + Text("stats.chart.reset".localized) + .font(.system(size: 8, weight: .semibold)) + .foregroundStyle(tint.opacity(0.9)) + .fixedSize() + .position(x: plot.maxX - 18, y: plot.minY - 8) + .opacity(nowX < plot.maxX - 100 ? 1 : 0) + Circle() + .fill(Color.white) + .overlay(Circle().stroke(tint, lineWidth: 2.5)) + .frame(width: 9, height: 9) + .position(x: nowX, y: nowY) + .scaleEffect(drawProgress >= 1 ? 1 : 0.01, anchor: .center) + .animation(animates ? .spring(response: 0.35, dampingFraction: 0.6) : nil, value: drawProgress >= 1) + ForEach(forecast.boosts, id: \.date) { boost in + Image(systemName: "arrow.up") + .font(.system(size: 7, weight: .black)) + .foregroundStyle(.white) + .frame(width: 13, height: 13) + .background(Circle().fill(Color.green)) + .position(x: x(boost.date, plot), y: plot.maxY) + .opacity(Double(drawProgress)) + .help("stats.chart.boost".localized) + } + if let runsOut = forecast.runsOutAt { + Image(systemName: "xmark") + .font(.system(size: 7, weight: .black)) + .foregroundStyle(.white) + .frame(width: 13, height: 13) + .background(Circle().fill(Color.red)) + .position(x: x(runsOut, plot), y: plot.maxY) + .opacity(Double(projectionProgress)) + } + } + } + + private func hoverOverlay(_ plot: CGRect, date: Date) -> some View { + let isFuture = date > forecast.now + let away = awayInterval(at: date) + let value: Double? = + away != nil + ? nil + : isFuture + ? forecast.pointsPerHour.map { + max(0, forecast.remaining - $0 * date.timeIntervalSince(forecast.now) / 3600) + } + : QuotaForecast.interpolatedRemaining(forecast.points, at: date) + let px = x(date, plot) + let label = + date.formatted( + forecast.isSession + ? .dateTime.hour().minute().locale(locale) + : .dateTime.weekday(.abbreviated).hour().minute().locale(locale)) + " · " + + (away.map(awayLabel) + ?? value.map { (isFuture ? "stats.chart.expected".localized : "") + UsageFormatters.percentage($0) } + ?? "—") + return ZStack(alignment: .topLeading) { + Path { path in + path.move(to: CGPoint(x: px, y: plot.minY)) + path.addLine(to: CGPoint(x: px, y: plot.maxY)) + } + .stroke(Color.primary.opacity(0.35), lineWidth: 1) + if let value { + Circle().fill(isFuture ? projectionTint : tint).frame(width: 6, height: 6) + .position(x: px, y: y(value, plot)) + } + Text(label) + .font(.system(size: 9, weight: .medium)).monospacedDigit() + .padding(.horizontal, 6).padding(.vertical, 3) + .background( + RoundedRectangle(cornerRadius: 5).fill(Theme.panelBackground) + .overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.primary.opacity(0.12))) + ) + .fixedSize() + .position(x: min(max(px, plot.minX + 55), plot.maxX - 55), y: plot.minY + 10) + } + .allowsHitTesting(false) + } + + private var accessibilitySummary: String { + let now = UsageFormatters.percentage(forecast.remaining) + guard let projected = forecast.projectedRemainingAtReset else { return now } + return now + " · " + "stats.chart.expected".localized + UsageFormatters.percentage(max(0, projected)) + } +} diff --git a/Sources/TokenGaugeApp/Views/HistoryModeControl.swift b/Sources/TokenGaugeApp/Views/HistoryModeControl.swift index 565496e..194774b 100644 --- a/Sources/TokenGaugeApp/Views/HistoryModeControl.swift +++ b/Sources/TokenGaugeApp/Views/HistoryModeControl.swift @@ -1,40 +1,120 @@ -import AppKit import SwiftUI -struct HistoryModeControl: NSViewRepresentable { +struct SegmentChrome: ViewModifier { + func body(content: Content) -> some View { + content + .padding(2) + .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(Color.primary.opacity(0.05))) + } +} + +struct SegmentFill: View { + let selected: Bool + let hovered: Bool + + var body: some View { + RoundedRectangle(cornerRadius: 6, style: .continuous) + .fill(Color.primary.opacity(selected ? 0.11 : hovered ? 0.05 : 0)) + .shadow(color: .black.opacity(selected ? 0.12 : 0), radius: 1.5, y: 0.5) + } +} + +struct HistoryModeControl: View { @Binding var mode: HistoryMode + @Environment(\.accessibilityReduceMotion) private var reduceMotion + @Environment(\.quotaAnimationsEnabled) private var animateChanges + @State private var hovered: HistoryMode? - func makeCoordinator() -> Coordinator { Coordinator(self) } - - func makeNSView(context: Context) -> NSSegmentedControl { - let control = NSSegmentedControl( - labels: HistoryMode.allCases.map { $0.titleKey.localized }, - trackingMode: .selectOne, target: context.coordinator, action: #selector(Coordinator.select(_:))) - HistoryControlAppearance.apply(to: control) - control.segmentDistribution = .fillEqually - control.selectedSegment = HistoryMode.allCases.firstIndex(of: mode) ?? 0 - control.setAccessibilityIdentifier("TokenGauge.historyMode.control") - control.setAccessibilityLabel("history.view".localized) - return control + var body: some View { + HStack(spacing: 2) { + ForEach(HistoryMode.allCases) { item in + let selected = item == mode + Button { + guard mode != item else { return } + withAnimation(animateChanges && !reduceMotion ? .easeOut(duration: 0.16) : nil) { mode = item } + } label: { + Text(item.titleKey.localized) + .font(.system(size: 10.5, weight: selected ? .semibold : .medium)) + .lineLimit(1) + .fixedSize() + .padding(.horizontal, 8) + .frame(height: Theme.Layout.historyModeSegmentHeight) + .background(SegmentFill(selected: selected, hovered: hovered == item)) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .foregroundStyle(selected ? .primary : .secondary) + .onHover { inside in + if inside { hovered = item } else if hovered == item { hovered = nil } + } + .accessibilityIdentifier("TokenGauge.historyMode.\(item.rawValue)") + .accessibilityAddTraits(selected ? [.isSelected] : []) + } + } + .modifier(SegmentChrome()) + .fixedSize() + .accessibilityElement(children: .contain) + .accessibilityIdentifier("TokenGauge.historyMode.control") + .accessibilityLabel("history.view".localized) } +} - func updateNSView(_ control: NSSegmentedControl, context: Context) { - context.coordinator.parent = self - for (index, item) in HistoryMode.allCases.enumerated() { - let label = item.titleKey.localized - if control.label(forSegment: index) != label { control.setLabel(label, forSegment: index) } +struct HistoryPeriodControl: View { + let title: String + let canGoBack: Bool + let canGoForward: Bool + let onPrevious: () -> Void + let onToday: () -> Void + let onNext: () -> Void + @State private var hovered: Int? + + var body: some View { + HStack(spacing: 0) { + chevron("chevron.left", index: 0, enabled: canGoBack, key: "history.previous", action: onPrevious) + divider + Button(action: onToday) { + Text(title) + .font(.system(size: 10.5, weight: .semibold)).monospacedDigit() + .lineLimit(1) + .fixedSize() + .contentTransition(.numericText()) + .padding(.horizontal, 8) + .frame(height: Theme.Layout.historyModeSegmentHeight) + .background(SegmentFill(selected: false, hovered: hovered == 1)) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .onHover { hovered = $0 ? 1 : (hovered == 1 ? nil : hovered) } + .help("history.return_today".localized) + divider + chevron("chevron.right", index: 2, enabled: canGoForward, key: "history.next", action: onNext) } - let selected = HistoryMode.allCases.firstIndex(of: mode) ?? 0 - if control.selectedSegment != selected { control.selectedSegment = selected } + .modifier(SegmentChrome()) + .fixedSize() + .accessibilityElement(children: .contain) + .accessibilityIdentifier("TokenGauge.historyPeriod.control") + .accessibilityLabel("history.period".localized) } - @MainActor final class Coordinator: NSObject { - var parent: HistoryModeControl - init(_ parent: HistoryModeControl) { self.parent = parent } + private var divider: some View { + Rectangle().fill(Color.primary.opacity(0.1)).frame(width: 1, height: 11) + } - @objc func select(_ control: NSSegmentedControl) { - guard HistoryMode.allCases.indices.contains(control.selectedSegment) else { return } - parent.mode = HistoryMode.allCases[control.selectedSegment] + private func chevron(_ symbol: String, index: Int, enabled: Bool, key: String, action: @escaping () -> Void) + -> some View + { + Button(action: action) { + Image(systemName: symbol) + .font(.system(size: 9, weight: .semibold)) + .frame(width: 19, height: Theme.Layout.historyModeSegmentHeight) + .background(SegmentFill(selected: false, hovered: enabled && hovered == index)) + .contentShape(Rectangle()) } + .buttonStyle(.plain) + .foregroundStyle(enabled ? .secondary : .quaternary) + .disabled(!enabled) + .onHover { hovered = $0 ? index : (hovered == index ? nil : hovered) } + .help(key.localized) + .accessibilityLabel(key.localized) } } diff --git a/Sources/TokenGaugeApp/Views/HistoryPanelView.swift b/Sources/TokenGaugeApp/Views/HistoryPanelView.swift index c527077..3b94460 100644 --- a/Sources/TokenGaugeApp/Views/HistoryPanelView.swift +++ b/Sources/TokenGaugeApp/Views/HistoryPanelView.swift @@ -70,10 +70,7 @@ struct HistoryPanelView: View { VStack(alignment: .leading, spacing: 6) { ViewThatFits(in: .horizontal) { historyToolbar(showsStreak: true) - VStack(alignment: .leading, spacing: 3) { - historyToolbar(showsStreak: false) - streakBadge - } + historyToolbar(showsStreak: false) } if model.loadFailed { Text("history.load_failed".localized).font(.caption).foregroundStyle(.secondary) @@ -90,7 +87,6 @@ struct HistoryPanelView: View { } .animation(motion, value: model.loadedMode) .animation(motion, value: model.periodStart) - resetLegend selectionSummary } } @@ -120,31 +116,37 @@ struct HistoryPanelView: View { } private func historyToolbar(showsStreak: Bool) -> some View { - HStack(spacing: 8) { + HStack(spacing: 0) { if mode == .recent { Text("history.heading".localized).font(.system(size: 11, weight: .semibold)) .fixedSize() } else { navigation } - Spacer(minLength: 0) + Spacer(minLength: 6) if showsStreak { streakBadge - Spacer(minLength: 0) + Spacer(minLength: 6) } modePicker } } private var streakBadge: some View { - HStack(spacing: 4) { - Image(systemName: "flame.fill").foregroundStyle(accent) - Text(streakLine).foregroundStyle(.secondary) + let streak = model.streak(for: providers) + return HStack(spacing: 3) { + Image(systemName: "flame.fill").foregroundStyle(streak.current > 0 ? accent : .secondary) + .symbolEffect(.bounce, value: streak.current) + Text("stats.days_value".localized(String(streak.current))) + .foregroundStyle(.primary.opacity(0.85)).monospacedDigit() + .contentTransition(.numericText()) } - .font(.system(size: 9, weight: .medium)) - .padding(.horizontal, 7).padding(.vertical, 3) - .background(Capsule().fill(Color.primary.opacity(0.045))) + .font(.system(size: 10.5, weight: .semibold)) + .padding(.horizontal, 8) + .frame(height: Theme.Layout.historyModeSegmentHeight + 4) + .background(Capsule().fill(accent.opacity(streak.current > 0 ? 0.1 : 0.04))) .fixedSize() + .help(streakLine) .accessibilityElement(children: .ignore).accessibilityLabel(streakLine) } @@ -189,9 +191,6 @@ struct HistoryPanelView: View { private var modePicker: some View { HistoryModeControl(mode: $mode) - .frame( - width: Theme.Layout.historyModeSegmentWidth * 3 + 4, - height: Theme.Layout.historyModeSegmentHeight + 4) } private var todayBadge: some View { @@ -205,14 +204,12 @@ struct HistoryPanelView: View { } private var navigation: some View { - let titleWidth: CGFloat = mode == .calendar ? 52 : 90 - return HistoryPeriodControl( - title: periodLabel, titleWidth: titleWidth, + HistoryPeriodControl( + title: periodLabel, canGoBack: model.canGoBack && !model.isLoading, canGoForward: model.canGoForward && !model.isLoading, onPrevious: { model.move(-1) }, onToday: returnToToday, onNext: { model.move(1) } ) - .frame(width: titleWidth + 48, height: Theme.Layout.historyModeSegmentHeight + 4) } private func returnToToday() { @@ -224,8 +221,12 @@ struct HistoryPanelView: View { private var periodLabel: String { if mode == .calendar { return model.periodStart.formatted(.dateTime.year().locale(locale)) } let last = Calendar.current.date(byAdding: .day, value: -1, to: model.periodEnd) ?? model.periodEnd - return model.periodStart.formatted(.dateTime.month(.abbreviated).day().locale(locale)) - + " – " + last.formatted(.dateTime.month(.abbreviated).day().locale(locale)) + guard Calendar.current.isDate(model.periodStart, equalTo: last, toGranularity: .month) else { + return model.periodStart.formatted(.dateTime.day().month(.abbreviated).locale(locale)) + + "–" + last.formatted(.dateTime.day().month(.abbreviated).locale(locale)) + } + return model.periodStart.formatted(.dateTime.day().locale(locale)) + "–" + + last.formatted(.dateTime.day().month(.abbreviated).locale(locale)) } private var bars: some View { @@ -351,34 +352,6 @@ struct HistoryPanelView: View { resets.filter { Calendar.current.isDate($0.date, inSameDayAs: day.date) } } - @ViewBuilder private var resetLegend: some View { - if !resets.isEmpty { - VStack(alignment: .leading, spacing: 3) { - Text("history.weekly_reset".localized) - .font(.system(size: 9, weight: .medium)).foregroundStyle(.secondary) - HStack(alignment: .top, spacing: 8) { - ForEach(providers, id: \.self) { provider in - if let reset = resets.first(where: { $0.provider == provider }) { - HStack(spacing: 4) { - RoundedRectangle(cornerRadius: 1).stroke(color(provider), lineWidth: 1.5) - .frame(width: 6, height: 6) - Text( - name(provider) + " · " - + reset.date.formatted( - .dateTime.weekday(.abbreviated).day().month(.abbreviated).hour().minute() - .locale(locale)) - ) - .font(.system(size: 9)).lineLimit(2) - } - .frame(maxWidth: .infinity, alignment: .leading) - .help("history.projection_help".localized) - } - } - } - } - } - } - private func weekdayLabel(_ weekday: Int) -> String { var calendar = Calendar.current calendar.locale = locale diff --git a/Sources/TokenGaugeApp/Views/HistoryPeriodControl.swift b/Sources/TokenGaugeApp/Views/HistoryPeriodControl.swift deleted file mode 100644 index 9e4f545..0000000 --- a/Sources/TokenGaugeApp/Views/HistoryPeriodControl.swift +++ /dev/null @@ -1,73 +0,0 @@ -import AppKit -import SwiftUI - -@MainActor -enum HistoryControlAppearance { - static func apply(to control: NSSegmentedControl) { - control.segmentStyle = .rounded - control.controlSize = .small - control.font = .systemFont(ofSize: 10) - control.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - } -} - -struct HistoryPeriodControl: NSViewRepresentable { - let title: String - let titleWidth: CGFloat - let canGoBack: Bool - let canGoForward: Bool - let onPrevious: () -> Void - let onToday: () -> Void - let onNext: () -> Void - - func makeCoordinator() -> Coordinator { Coordinator(self) } - - func makeNSView(context: Context) -> NSSegmentedControl { - let control = NSSegmentedControl( - labels: ["", title, ""], trackingMode: .momentary, - target: context.coordinator, action: #selector(Coordinator.select(_:))) - HistoryControlAppearance.apply(to: control) - control.segmentDistribution = .fill - control.setWidth(22, forSegment: 0) - control.setWidth(titleWidth, forSegment: 1) - control.setWidth(22, forSegment: 2) - for (index, symbol, key) in [(0, "chevron.left", "history.previous"), (2, "chevron.right", "history.next")] { - let image = NSImage(systemSymbolName: symbol, accessibilityDescription: key.localized) - image?.size = NSSize(width: 8, height: 10) - control.setImage(image, forSegment: index) - control.setImageScaling(.scaleProportionallyDown, forSegment: index) - control.setToolTip(key.localized, forSegment: index) - } - control.setToolTip("history.return_today".localized, forSegment: 1) - control.setAccessibilityIdentifier("TokenGauge.historyPeriod.control") - control.setAccessibilityLabel("history.period".localized) - update(control) - return control - } - - func updateNSView(_ control: NSSegmentedControl, context: Context) { - context.coordinator.parent = self - update(control) - } - - private func update(_ control: NSSegmentedControl) { - if control.label(forSegment: 1) != title { control.setLabel(title, forSegment: 1) } - if control.width(forSegment: 1) != titleWidth { control.setWidth(titleWidth, forSegment: 1) } - control.setEnabled(canGoBack, forSegment: 0) - control.setEnabled(canGoForward, forSegment: 2) - } - - @MainActor final class Coordinator: NSObject { - var parent: HistoryPeriodControl - init(_ parent: HistoryPeriodControl) { self.parent = parent } - - @objc func select(_ control: NSSegmentedControl) { - switch control.selectedSegment { - case 0: if parent.canGoBack { parent.onPrevious() } - case 1: parent.onToday() - case 2: if parent.canGoForward { parent.onNext() } - default: break - } - } - } -} diff --git a/Sources/TokenGaugeApp/Views/PopoverView.swift b/Sources/TokenGaugeApp/Views/PopoverView.swift index 3da5776..67b813e 100644 --- a/Sources/TokenGaugeApp/Views/PopoverView.swift +++ b/Sources/TokenGaugeApp/Views/PopoverView.swift @@ -10,6 +10,8 @@ struct PopoverView: View { @ObservedObject private var updates = UpdateManager.shared @ObservedObject private var localization = LocalizationManager.shared @StateObject private var history: HistoryDashboardModel + @StateObject private var stats: StatsModel + @State private var showsStats = false init( store: UsageStore, showSettings: @escaping () -> Void, showAbout: @escaping () -> Void, @@ -21,8 +23,11 @@ struct PopoverView: View { let preview = store.historyReadsEnabled ? nil : [store.claude.snapshot, store.codex.snapshot].compactMap { $0 } _history = StateObject( wrappedValue: history ?? HistoryDashboardModel(previewSnapshots: preview, mode: store.historyMode)) + _stats = StateObject(wrappedValue: StatsModel(preview: preview.map(StatsModel.preview(snapshots:)))) } + private var animatesMotion: Bool { store.animateChanges && !reduceMotion } + var providerMaxHeight: CGFloat { store.panelStyle == .rings ? Theme.Layout.ringProviderMaxHeight @@ -66,7 +71,24 @@ struct PopoverView: View { UpdateBannerView(updates: updates) } providerPicker + FlipCard(flipped: showsStats, animated: animatesMotion) { + panelFace + } back: { + StatsView(store: store, model: stats, providers: store.displayMode.providers) + .task(id: store.historyRevision) { await stats.load(revision: store.historyRevision) } + } + } + .padding(.horizontal, Theme.Layout.panelPadding) + .padding(.top, 12) + .padding(.bottom, Theme.Layout.panelBottomPadding) + .frame(width: panelWidth) + .environment(\.quotaAnimationsEnabled, store.animateChanges) + .fixedSize(horizontal: false, vertical: true) + .id(localization.language) + } + private var panelFace: some View { + VStack(alignment: .leading, spacing: Theme.Layout.sectionSpacing) { ViewThatFits(in: .vertical) { providerContent ScrollView { @@ -76,18 +98,28 @@ struct PopoverView: View { .frame(height: providerMaxHeight) } .frame(maxHeight: providerMaxHeight) - .animation(store.animateChanges && !reduceMotion ? Theme.Motion.content : nil, value: store.panelStyle) + .animation(animatesMotion ? Theme.Motion.content : nil, value: store.panelStyle) HistoryPanelView( model: history, mode: $store.historyMode, providers: store.displayMode.providers, resets: upcomingResets, compact: store.panelStyle != .standard) } - .padding(.horizontal, Theme.Layout.panelPadding) - .padding(.top, 12) - .padding(.bottom, Theme.Layout.panelBottomPadding) - .frame(width: panelWidth) - .environment(\.quotaAnimationsEnabled, store.animateChanges) - .fixedSize(horizontal: false, vertical: true) - .id(localization.language) + } + + private var statsButton: some View { + Button { + showsStats.toggle() + } label: { + Image(systemName: showsStats ? "gauge.with.dots.needle.67percent" : "chart.xyaxis.line") + .font(.system(size: 12, weight: showsStats ? .semibold : .regular)) + .foregroundStyle(showsStats ? AnyShapeStyle(Theme.claude) : AnyShapeStyle(.secondary)) + .contentTransition(.symbolEffect(.replace)) + .frame(width: 18, height: 16) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .help((showsStats ? "stats.close" : "stats.open").localized) + .accessibilityLabel((showsStats ? "stats.close" : "stats.open").localized) + .accessibilityIdentifier("TokenGauge.stats") } var upcomingResets: [HistoryReset] { @@ -159,7 +191,8 @@ struct PopoverView: View { hidesAccountLabel: store.hideAccountLabel, claudeAutomaticRecovery: store.claudeAutomaticRecovery, showHourlyPace: store.showHourlyPace, - stretchesHeight: store.displayMode == .unified && store.panelStyle == .rings, + showsSessionForecast: store.showHourlyPace && store.displayMode != .unified, + stretchesHeight: store.displayMode == .unified && store.panelStyle != .compact, accountLabel: store.claudeAccountLabel, paces: paces(for: provider), previousPaces: previousPaces(for: provider) ) @@ -235,6 +268,8 @@ struct PopoverView: View { Spacer(minLength: 0) + statsButton + Menu { Picker("settings.panel_style".localized, selection: $store.panelStyle) { ForEach(QuotaPanelStyle.allCases) { style in diff --git a/Sources/TokenGaugeApp/Views/ProviderCard.swift b/Sources/TokenGaugeApp/Views/ProviderCard.swift index 99216ef..25c7091 100644 --- a/Sources/TokenGaugeApp/Views/ProviderCard.swift +++ b/Sources/TokenGaugeApp/Views/ProviderCard.swift @@ -13,6 +13,7 @@ struct ProviderCard: View { var hidesAccountLabel = false var claudeAutomaticRecovery = false var showHourlyPace = false + var showsSessionForecast = false var stretchesHeight = false var accountLabel: String? var paces: [String: QuotaPace] = [:] @@ -76,6 +77,7 @@ struct ProviderCard: View { QuotaRingWindow( window: window, tint: tint, chips: chips(for: window), historical: showsLastKnown, showPace: canShowPace(window), pace: paces[window.id], previousPace: previousPaces[window.id], + session: sessionForecast(window), metrics: .single, alignsTitleRows: false) } else { ViewThatFits(in: .horizontal) { @@ -89,7 +91,8 @@ struct ProviderCard: View { if panelStyle == .compact { CompactQuotaWindowRow( window: window, tint: tint, chips: chips(for: window), historical: showsLastKnown, - showPace: canShowPace(window), pace: paces[window.id], previousPace: previousPaces[window.id]) + showPace: canShowPace(window), pace: paces[window.id], previousPace: previousPaces[window.id], + session: sessionForecast(window)) } else { QuotaWindowRow( window: window, @@ -97,7 +100,8 @@ struct ProviderCard: View { chips: chips(for: window), prominent: index == 0 && !showsLastKnown, historical: showsLastKnown, - showPace: canShowPace(window), pace: paces[window.id], previousPace: previousPaces[window.id] + showPace: canShowPace(window), pace: paces[window.id], previousPace: previousPaces[window.id], + session: sessionForecast(window) ) } } @@ -109,7 +113,8 @@ struct ProviderCard: View { ForEach(visibleWindows) { window in QuotaRingWindow( window: window, tint: tint, chips: chips(for: window), historical: showsLastKnown, - showPace: canShowPace(window), pace: paces[window.id], previousPace: previousPaces[window.id]) + showPace: canShowPace(window), pace: paces[window.id], previousPace: previousPaces[window.id], + session: sessionForecast(window)) } } } @@ -120,7 +125,8 @@ struct ProviderCard: View { if index > 0 { Divider() } QuotaRingRow( window: window, tint: tint, chips: chips(for: window), historical: showsLastKnown, - showPace: canShowPace(window), pace: paces[window.id], previousPace: previousPaces[window.id]) + showPace: canShowPace(window), pace: paces[window.id], previousPace: previousPaces[window.id], + session: sessionForecast(window)) } } } @@ -129,6 +135,17 @@ struct ProviderCard: View { showHourlyPace && (state.status == .ready || showsLastKnown) && window.durationMinutes == 10080 } + private func sessionForecast(_ window: QuotaWindow) -> QuotaForecast? { + guard showsSessionForecast, state.status == .ready, window.durationMinutes == 300, + let forecast = QuotaForecast.make(provider: provider, window: window, rows: [], now: Date()), + forecast.pointsPerHour != nil + else { return nil } + switch forecast.verdict { + case .lasts, .tight, .runsOut: return forecast + case .noUsage, .exhausted: return nil + } + } + private var compactHeader: some View { HStack(spacing: 7) { ProviderLogo(provider: provider, size: 13) diff --git a/Sources/TokenGaugeApp/Views/QuotaPaceLabel.swift b/Sources/TokenGaugeApp/Views/QuotaPaceLabel.swift index 831cb48..6ddd5cf 100644 --- a/Sources/TokenGaugeApp/Views/QuotaPaceLabel.swift +++ b/Sources/TokenGaugeApp/Views/QuotaPaceLabel.swift @@ -41,6 +41,49 @@ struct QuotaPaceLabel: View { } } +struct SessionForecastLabel: View { + let forecast: QuotaForecast + + private var pace: String { UsageFormatters.percentage(forecast.pointsPerHour ?? 0) } + + private var style: (symbol: String, tint: Color, text: String, detail: String) { + switch forecast.verdict { + case .runsOut(let date): + let time = date.formatted( + .dateTime.hour().minute().locale(Locale(identifier: LocalizationManager.shared.language.rawValue))) + return ( + "exclamationmark.triangle.fill", .red, "session.runs_out".localized(time), + "session.detail_runs_out".localized( + pace, UsageFormatters.duration(forecast.reset.timeIntervalSince(date))) + ) + case .tight(let margin): + return ( + "exclamationmark.circle.fill", .orange, "session.tight".localized(pace), + "session.detail".localized(pace, UsageFormatters.percentage(max(0, margin))) + ) + case .lasts(let margin): + return ( + "checkmark.circle.fill", .green, "session.lasts".localized(pace), + "session.detail".localized(pace, UsageFormatters.percentage(margin)) + ) + case .noUsage, .exhausted: return ("", .clear, "", "") + } + } + + var body: some View { + let style = style + HStack(alignment: .firstTextBaseline, spacing: 3) { + Image(systemName: style.symbol).font(.system(size: 9, weight: .semibold)).foregroundStyle(style.tint) + .accessibilityHidden(true) + Text(style.text).monospacedDigit().lineLimit(1) + } + .font(.system(size: 10)).foregroundStyle(.secondary) + .help(style.detail) + .accessibilityElement(children: .ignore) + .accessibilityLabel(style.detail) + } +} + struct QuotaPaceDetailsView: View { let display: QuotaPaceDisplay private var locale: Locale { Locale(identifier: LocalizationManager.shared.language.rawValue) } diff --git a/Sources/TokenGaugeApp/Views/QuotaRing.swift b/Sources/TokenGaugeApp/Views/QuotaRing.swift index 927a258..aa43e7b 100644 --- a/Sources/TokenGaugeApp/Views/QuotaRing.swift +++ b/Sources/TokenGaugeApp/Views/QuotaRing.swift @@ -47,6 +47,7 @@ struct QuotaRingWindow: View { var showPace = false var pace: QuotaPace? var previousPace: QuotaPace? + var session: QuotaForecast? var metrics: QuotaRingMetrics = .grid var alignsTitleRows = true @@ -71,6 +72,7 @@ struct QuotaRingWindow: View { .truncationMode(.tail) .multilineTextAlignment(.center) if showPace { QuotaPaceLabel(pace: pace, previousPace: previousPace) } + if let session { SessionForecastLabel(forecast: session) } if let summary = UsageFormatters.modelChips(chips) { Text(summary) .font(.system(size: 10)) @@ -84,7 +86,7 @@ struct QuotaRingWindow: View { } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - .accessibilityElement(children: showPace ? .contain : .combine) + .accessibilityElement(children: showPace || session != nil ? .contain : .combine) } } @@ -96,6 +98,7 @@ struct QuotaRingRow: View { var showPace = false var pace: QuotaPace? var previousPace: QuotaPace? + var session: QuotaForecast? var body: some View { HStack(spacing: Theme.Layout.quotaRingSpacing) { @@ -123,18 +126,11 @@ struct QuotaRingRow: View { .lineLimit(1) .truncationMode(.tail) if showPace { QuotaPaceLabel(pace: pace, previousPace: previousPace) } - if let summary = UsageFormatters.modelChips(chips) { - Text(summary) - .font(.system(size: 10)) - .foregroundStyle(.secondary) - .lineLimit(1) - .truncationMode(.tail) - .help(summary) - .monospacedDigit() - } + if let session { SessionForecastLabel(forecast: session) } + ModelChipsLabel(chips: chips) } .frame(maxWidth: .infinity, alignment: .leading) } - .accessibilityElement(children: showPace ? .contain : .combine) + .accessibilityElement(children: showPace || session != nil ? .contain : .combine) } } diff --git a/Sources/TokenGaugeApp/Views/QuotaWindowRow.swift b/Sources/TokenGaugeApp/Views/QuotaWindowRow.swift index 762d781..abf6bd4 100644 --- a/Sources/TokenGaugeApp/Views/QuotaWindowRow.swift +++ b/Sources/TokenGaugeApp/Views/QuotaWindowRow.swift @@ -10,6 +10,7 @@ struct QuotaWindowRow: View { var showPace = false var pace: QuotaPace? var previousPace: QuotaPace? + var session: QuotaForecast? private var valueColor: Color { Theme.severity(remaining: window.remainingPercentage) ?? .primary @@ -50,17 +51,33 @@ struct QuotaWindowRow: View { .foregroundStyle(.secondary) .lineLimit(1) Spacer(minLength: 4) - if let summary = UsageFormatters.modelChips(chips) { - Text(summary) - .font(.system(size: 10)) - .foregroundStyle(.secondary) - .lineLimit(1) - .truncationMode(.middle) - .monospacedDigit() - .layoutPriority(-1) - } + ModelChipsLabel(chips: chips).layoutPriority(-1) } if showPace { QuotaPaceLabel(pace: pace, previousPace: previousPace) } + if let session { SessionForecastLabel(forecast: session) } } } } + +struct ModelChipsLabel: View { + let chips: [ModelUsageChip] + + var body: some View { + if let summary = UsageFormatters.modelChips(chips) { + ViewThatFits(in: .horizontal) { + line(chips) + line(Array(chips.prefix(1))) + } + .help(summary) + } + } + + private func line(_ chips: [ModelUsageChip]) -> some View { + Text(UsageFormatters.modelChips(chips) ?? "") + .font(.system(size: 10)) + .foregroundStyle(.secondary) + .lineLimit(1) + .truncationMode(.middle) + .monospacedDigit() + } +} diff --git a/Sources/TokenGaugeApp/Views/StatsBarsView.swift b/Sources/TokenGaugeApp/Views/StatsBarsView.swift new file mode 100644 index 0000000..71ddcb8 --- /dev/null +++ b/Sources/TokenGaugeApp/Views/StatsBarsView.swift @@ -0,0 +1,134 @@ +import SwiftUI +import TokenGaugeCore + +struct StatsBarsView: View { + let days: [StatsDayTotal] + let typical: Int? + let providers: [UsageProvider] + let locale: Locale + let tint: Color + @Environment(\.accessibilityReduceMotion) private var reduceMotion + @Environment(\.quotaAnimationsEnabled) private var animateChanges + @State private var grown = false + @State private var hovered: String? + + private let chartHeight: CGFloat = 70 + + private var maximum: Int { max(1, days.compactMap(\.total).max() ?? 1, typical ?? 0) } + private var focus: StatsDayTotal? { days.first { $0.id == hovered } ?? days.last } + + static func color(_ segment: StatsSegment, providers: [UsageProvider]) -> Color { + if segment.id == StatsSummary.otherLabel { return Color.secondary.opacity(0.35) } + let base = segment.provider == .claude ? Theme.claude : Theme.codex + if providers.count > 1 { return base } + switch segment.rank { + case 0: return base + case 1: return Color(nsColor: NSColor(base).blended(withFraction: 0.45, of: .white) ?? NSColor(base)) + default: return base.opacity(0.4) + } + } + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + caption + ZStack(alignment: .bottomLeading) { + HStack(alignment: .bottom, spacing: 4) { + ForEach(Array(days.enumerated()), id: \.element.id) { index, day in + bar(day, index: index) + } + } + .frame(height: chartHeight, alignment: .bottom) + if let typical, typical > 0 { + typicalLine(typical) + } + } + HStack(spacing: 4) { + ForEach(days) { day in + Text(day.date.formatted(.dateTime.weekday(.narrow).locale(locale))) + .font(.system(size: 8, weight: day.id == days.last?.id ? .semibold : .regular)) + .foregroundStyle(day.id == focus?.id ? AnyShapeStyle(.primary) : AnyShapeStyle(.tertiary)) + .frame(maxWidth: .infinity) + } + } + } + .onAppear { + guard !grown else { return } + if animateChanges && !reduceMotion { + withAnimation(.spring(response: 0.6, dampingFraction: 0.82).delay(0.15)) { grown = true } + } else { + grown = true + } + } + } + + private var caption: some View { + HStack(spacing: 6) { + if let focus { + Text(focus.date.formatted(.dateTime.weekday(.wide).day().month(.abbreviated).locale(locale))) + .font(.system(size: 9.5)).foregroundStyle(.secondary) + Spacer(minLength: 4) + Text(focus.total.map(UsageFormatters.tokens) ?? "history.unknown".localized) + .font(.system(size: 11, weight: .semibold)).monospacedDigit() + .contentTransition(.numericText()) + } + } + .animation(.easeOut(duration: 0.15), value: focus?.id) + } + + private func bar(_ day: StatsDayTotal, index: Int) -> some View { + let height = CGFloat(day.total ?? 0) / CGFloat(maximum) * chartHeight + let isFocus = day.id == focus?.id + return VStack(spacing: 0) { + Spacer(minLength: 0) + VStack(spacing: 1) { + ForEach(day.segments) { segment in + Rectangle() + .fill(Self.color(segment, providers: providers)) + .frame(height: max(0, height * CGFloat(segment.tokens) / CGFloat(max(1, day.total ?? 1)) - 1)) + } + } + .frame(height: max(day.total == nil ? 0 : 2, height), alignment: .bottom) + .clipShape(RoundedRectangle(cornerRadius: 3, style: .continuous)) + .scaleEffect(y: grown || !(animateChanges && !reduceMotion) ? 1 : 0.02, anchor: .bottom) + .animation( + animateChanges && !reduceMotion + ? .spring(response: 0.55, dampingFraction: 0.8).delay(0.012 * Double(index)) : nil, + value: grown) + if day.total == nil { + Capsule().fill(Color.secondary.opacity(0.25)).frame(width: 5, height: 1) + } + } + .frame(maxWidth: .infinity) + .frame(height: chartHeight) + .background( + RoundedRectangle(cornerRadius: 4).fill(tint.opacity(isFocus && hovered != nil ? 0.08 : 0)) + ) + .contentShape(Rectangle()) + .onHover { inside in + if inside { hovered = day.id } else if hovered == day.id { hovered = nil } + } + .accessibilityElement(children: .ignore) + .accessibilityLabel( + day.date.formatted(.dateTime.weekday(.wide).day().month().locale(locale)) + ": " + + (day.total.map(UsageFormatters.tokens) ?? "history.unknown".localized)) + } + + private func typicalLine(_ typical: Int) -> some View { + let y = chartHeight - CGFloat(typical) / CGFloat(maximum) * chartHeight + return ZStack(alignment: .topLeading) { + Path { path in + path.move(to: CGPoint(x: 0, y: y)) + path.addLine(to: CGPoint(x: 2_000, y: y)) + } + .stroke(Color.primary.opacity(0.3), style: StrokeStyle(lineWidth: 1, dash: [3, 3])) + Text("stats.daily.typical".localized) + .font(.system(size: 7.5, weight: .medium)).foregroundStyle(.secondary) + .padding(.horizontal, 3) + .background(Theme.panelBackground.opacity(0.85)) + .offset(y: y - 12) + } + .frame(height: chartHeight) + .clipped() + .allowsHitTesting(false) + } +} diff --git a/Sources/TokenGaugeApp/Views/StatsView.swift b/Sources/TokenGaugeApp/Views/StatsView.swift new file mode 100644 index 0000000..c64c975 --- /dev/null +++ b/Sources/TokenGaugeApp/Views/StatsView.swift @@ -0,0 +1,580 @@ +import SwiftUI +import TokenGaugeCore + +struct StatsView: View { + @ObservedObject var store: UsageStore + @ObservedObject var model: StatsModel + let providers: [UsageProvider] + var scrolls = true + @Environment(\.accessibilityReduceMotion) private var reduceMotion + @Environment(\.quotaAnimationsEnabled) private var animateChanges + @State private var selectedForecast: String? + @State private var appeared = false + + private var locale: Locale { Locale(identifier: LocalizationManager.shared.language.rawValue) } + private var animates: Bool { animateChanges && !reduceMotion } + private var shown: Bool { appeared || !animates } + private var accent: Color { providers == [.codex] ? Theme.codex : Theme.claude } + + private var forecasts: [QuotaForecast] { + let states = providers.compactMap { provider -> (UsageProvider, [QuotaWindow])? in + let state = store.state(for: provider) + guard state.status == .ready, let snapshot = state.snapshot else { return nil } + return ( + provider, + WindowVisibility.visible( + snapshot.windows, provider: provider, showLunaReserve: store.showLunaReserve, + hiddenClaudeWindows: store.hiddenClaudeWindows) + ) + } + return model.forecasts(for: states, accountFingerprint: store.claudeAccountFingerprint) + .sorted { urgency($0) < urgency($1) } + } + + private var summary: StatsSummary? { + model.summary(for: providers, codexSummary: store.codex.snapshot?.summary) + } + + var body: some View { + Group { + if scrolls { + ScrollView { sections }.scrollIndicators(.never) + } else { + sections + } + } + .onAppear { + guard !appeared else { return } + if animates { withAnimation(.easeOut(duration: 0.35)) { appeared = true } } else { appeared = true } + } + } + + private var sections: some View { + VStack(alignment: .leading, spacing: 10) { + if model.loadFailed { + placeholder("stats.load_failed".localized, symbol: "exclamationmark.triangle") + } else if model.records == nil { + placeholder("stats.loading".localized, symbol: "chart.xyaxis.line") + } else { + let forecasts = forecasts + section(0) { forecastCard(forecasts) } + if let summary { + section(1) { todayCard(summary) } + section(2) { totalsGrid(summary) } + section(3) { dailyCard(summary) } + if summary.turns != nil || summary.cachedShare != nil { + section(4) { activityCard(summary) } + } + section(5) { rankings(summary) } + } + } + } + .padding(.bottom, 4) + } + + private func section(_ index: Int, @ViewBuilder content: () -> Content) -> some View { + content() + .opacity(shown ? 1 : 0) + .offset(y: shown ? 0 : 10) + .animation( + animates ? .spring(response: 0.42, dampingFraction: 0.85).delay(0.05 * Double(index)) : nil, + value: appeared) + } + + private func placeholder(_ text: String, symbol: String) -> some View { + VStack(spacing: 8) { + Image(systemName: symbol).font(.system(size: 20, weight: .light)).accessibilityHidden(true).foregroundStyle( + .secondary) + Text(text).font(.system(size: 11)).foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity).frame(height: 180) + } + + private func card( + _ id: StatsCard, _ title: String, symbol: String, trailing: String? = nil, + @ViewBuilder content: () -> Content + ) -> some View { + let collapsed = store.collapsedStatsCards.contains(id) + return VStack(alignment: .leading, spacing: 9) { + Button { + withAnimation(animates ? .spring(response: 0.34, dampingFraction: 0.88) : nil) { + store.toggleStatsCard(id) + } + } label: { + HStack(spacing: 6) { + Image(systemName: symbol).font(.system(size: 10, weight: .semibold)).accessibilityHidden(true) + .foregroundStyle(accent) + Text(title).font(.system(size: 11, weight: .semibold)) + Spacer(minLength: 4) + if let trailing { + Text(trailing).font(.system(size: 9, weight: .medium)).foregroundStyle(.secondary) + .lineLimit(1) + } + Image(systemName: "chevron.down") + .font(.system(size: 8.5, weight: .semibold)) + .foregroundStyle(.tertiary) + .rotationEffect(.degrees(collapsed ? -90 : 0)) + .accessibilityHidden(true) + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .help((collapsed ? "stats.card.expand" : "stats.card.collapse").localized) + .accessibilityValue((collapsed ? "stats.card.collapsed" : "stats.card.expanded").localized) + if !collapsed { + content().transition(.opacity) + } + } + .padding(Theme.Layout.cardPadding) + .frame(maxWidth: .infinity, alignment: .leading) + .clipped() + .providerCard() + } + + private func urgency(_ forecast: QuotaForecast) -> Double { + switch forecast.verdict { + case .exhausted: return -1_000 + case .runsOut(let date): return -100 + date.timeIntervalSince(forecast.now) / 86_400 + case .tight(let margin), .lasts(let margin): return margin + case .noUsage: return 1_000 + } + } + + @ViewBuilder private func forecastCard(_ forecasts: [QuotaForecast]) -> some View { + let selected = forecasts.first { $0.id == selectedForecast } ?? forecasts.first + card(.forecast, "stats.forecast.title".localized, symbol: "gauge.with.needle") { + if let selected { + VStack(alignment: .leading, spacing: 10) { + headline(selected) + ForecastChartView(forecast: selected, tint: color(selected.provider), locale: locale) + .id(selected.id) + .transition(.opacity) + paceLine(selected) + if forecasts.count > 1 { + VStack(spacing: 4) { + ForEach(forecasts) { forecast in + forecastRow(forecast, selected: forecast.id == selected.id) + } + } + } + } + .animation(animates ? .easeInOut(duration: 0.22) : nil, value: selected.id) + } else { + Text("stats.forecast.unavailable".localized) + .font(.system(size: 11)).foregroundStyle(.secondary) + } + } + .help((selected?.isSession == true ? "stats.forecast.help_session" : "stats.forecast.help").localized) + } + + private func headline(_ forecast: QuotaForecast) -> some View { + let (symbol, tint) = verdictStyle(forecast.verdict) + return HStack(alignment: .firstTextBaseline, spacing: 8) { + Image(systemName: symbol).font(.system(size: 15, weight: .semibold)).accessibilityHidden(true) + .foregroundStyle(tint) + .symbolRenderingMode(.hierarchical) + VStack(alignment: .leading, spacing: 2) { + Text(headlineText(forecast.verdict)) + .font(.system(size: 17, weight: .bold)) + .foregroundStyle(tint) + .contentTransition(.opacity) + Text(verdictText(forecast)) + .font(.system(size: 10.5, weight: .medium)).foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + Spacer(minLength: 4) + VStack(alignment: .trailing, spacing: 2) { + HStack(spacing: 4) { + ProviderLogo(provider: forecast.provider, size: 11) + Text(windowName(forecast)).font(.system(size: 10, weight: .semibold)).lineLimit(1) + } + Text(UsageFormatters.reset(forecast.reset)) + .font(.system(size: 9)).foregroundStyle(.secondary).lineLimit(1) + } + } + } + + private func paceLine(_ forecast: QuotaForecast) -> some View { + HStack(spacing: 10) { + let session = forecast.isSession + if let average = forecast.pointsPerHour { + label( + "speedometer", + (session ? "stats.pace.session" : "stats.pace.average").localized(pace(average, forecast))) + } + if let recent = forecast.recentPointsPerHour { + label( + "clock.arrow.circlepath", + (session ? "stats.pace.hour" : "stats.pace.today").localized(pace(recent, forecast))) + } + Spacer(minLength: 0) + if forecast.remaining > 0, forecast.pointsPerHour != nil { + label( + session ? "hourglass" : "calendar", + (session ? "stats.budget.hour" : "stats.budget").localized( + UsageFormatters.percentage(forecast.budget))) + } + } + .font(.system(size: 9.5)) + .foregroundStyle(.secondary) + } + + private func label(_ symbol: String, _ text: String) -> some View { + HStack(spacing: 3) { + Image(systemName: symbol).font(.system(size: 8.5)).accessibilityHidden(true) + Text(text).lineLimit(1).monospacedDigit() + } + } + + private func forecastRow(_ forecast: QuotaForecast, selected: Bool) -> some View { + let (symbol, tint) = verdictStyle(forecast.verdict) + return Button { + selectedForecast = forecast.id + } label: { + HStack(spacing: 7) { + ProviderLogo(provider: forecast.provider, size: 11) + Text(windowName(forecast)).font(.system(size: 10.5, weight: .medium)).lineLimit(1) + Spacer(minLength: 6) + Image(systemName: symbol).font(.system(size: 9, weight: .semibold)).accessibilityHidden(true) + .foregroundStyle(tint) + Text(shortVerdict(forecast)).font(.system(size: 10)).foregroundStyle(.secondary).lineLimit(1) + .monospacedDigit() + } + .padding(.horizontal, 8).frame(height: 24) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(selected ? color(forecast.provider).opacity(0.12) : Color.primary.opacity(0.03)) + ) + .overlay( + RoundedRectangle(cornerRadius: 6) + .stroke(selected ? color(forecast.provider).opacity(0.35) : .clear, lineWidth: 1) + ) + .contentShape(Rectangle()) + } + .buttonStyle(PressableStyle()) + .accessibilityAddTraits(selected ? [.isSelected] : []) + } + + private func verdictStyle(_ verdict: QuotaForecast.Verdict) -> (String, Color) { + switch verdict { + case .lasts: return ("checkmark.seal.fill", .green) + case .tight: return ("exclamationmark.circle.fill", .orange) + case .runsOut: return ("exclamationmark.triangle.fill", .red) + case .exhausted: return ("xmark.octagon.fill", .red) + case .noUsage: return ("moon.zzz.fill", .secondary) + } + } + + private func headlineText(_ verdict: QuotaForecast.Verdict) -> String { + switch verdict { + case .lasts: return "stats.headline.lasts".localized + case .tight: return "stats.headline.tight".localized + case .runsOut: return "stats.headline.runs_out".localized + case .exhausted: return "stats.headline.exhausted".localized + case .noUsage: return "stats.headline.no_usage".localized + } + } + + private func verdictText(_ forecast: QuotaForecast) -> String { + switch forecast.verdict { + case .lasts(let margin): return "stats.verdict.lasts".localized(UsageFormatters.percentage(margin)) + case .tight(let margin): return "stats.verdict.tight".localized(UsageFormatters.percentage(max(0, margin))) + case .runsOut(let date): + return (forecast.isSession ? "stats.verdict.runs_out_session" : "stats.verdict.runs_out").localized( + moment(date, forecast), + UsageFormatters.duration(forecast.reset.timeIntervalSince(date))) + case .exhausted: return "stats.verdict.exhausted".localized + case .noUsage: return "stats.verdict.no_usage".localized + } + } + + private func shortVerdict(_ forecast: QuotaForecast) -> String { + switch forecast.verdict { + case .lasts(let margin), .tight(let margin): + return "stats.row.left".localized(UsageFormatters.percentage(max(0, margin))) + case .runsOut(let date): return moment(date, forecast) + case .exhausted: return "stats.headline.exhausted".localized + case .noUsage: return UsageFormatters.percentage(forecast.remaining) + } + } + + private func windowName(_ forecast: QuotaForecast) -> String { + UsageFormatters.windowName( + QuotaWindow( + id: forecast.windowID, usedPercentage: 100 - forecast.remaining, resetsAt: forecast.reset, + durationMinutes: Int(forecast.reset.timeIntervalSince(forecast.start) / 60), + displayName: forecast.displayName)) + } + + private func pace(_ value: Double, _ forecast: QuotaForecast) -> String { + UsageFormatters.percentage(value * forecast.budgetUnit / 3600) + } + + private func moment(_ date: Date, _ forecast: QuotaForecast) -> String { + date.formatted( + forecast.isSession + ? .dateTime.hour().minute().locale(locale) + : .dateTime.weekday(.abbreviated).hour().minute().locale(locale)) + } + + private func todayCard(_ summary: StatsSummary) -> some View { + card( + .today, "stats.today.title".localized, symbol: "sun.max.fill", + trailing: summary.typicalDay.map { "stats.today.typical".localized(UsageFormatters.tokens($0)) } + ) { + VStack(alignment: .leading, spacing: 8) { + HStack(alignment: .firstTextBaseline, spacing: 8) { + Text(summary.today.map(UsageFormatters.tokens) ?? "—") + .font(.system(size: 24, weight: .bold, design: .rounded)).monospacedDigit() + .contentTransition(.numericText()) + if let ratio = summary.ratio { + Text("stats.today.ratio".localized(ratioText(ratio))) + .font(.system(size: 10, weight: .semibold)).monospacedDigit() + .foregroundStyle(ratioColor(ratio)) + .padding(.horizontal, 7).padding(.vertical, 3) + .background(Capsule().fill(ratioColor(ratio).opacity(0.13))) + } + Spacer(minLength: 0) + } + if let insight = summary.insight { + HStack(alignment: .top, spacing: 6) { + Image(systemName: insightSymbol(insight)).font(.system(size: 10, weight: .semibold)) + .accessibilityHidden(true) + .foregroundStyle(insightColor(insight)) + Text(insightText(insight)).font(.system(size: 10.5)).foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + } + } + } + } + + private func ratioText(_ ratio: Double) -> String { + ratio.formatted(.number.precision(.fractionLength(ratio >= 10 ? 0 : 1)).locale(locale)) + } + + private func ratioColor(_ ratio: Double) -> Color { + if ratio >= 1.8 { return .orange } + if ratio <= 0.4 { return Theme.codex } + return .secondary + } + + private func insightSymbol(_ insight: StatsInsight) -> String { + switch insight { + case .newModel: return "sparkles" + case .spike: return "flame.fill" + case .quiet: return "moon.fill" + case .steady: return "checkmark.circle.fill" + } + } + + private func insightColor(_ insight: StatsInsight) -> Color { + switch insight { + case .newModel: return .purple + case .spike: return .orange + case .quiet: return Theme.codex + case .steady: return .green + } + } + + private func insightText(_ insight: StatsInsight) -> String { + switch insight { + case .newModel(let model): return "stats.insight.new_model".localized(model) + case .spike: return "stats.insight.spike".localized + case .quiet: return "stats.insight.quiet".localized + case .steady: return "stats.insight.steady".localized + } + } + + private func totalsGrid(_ summary: StatsSummary) -> some View { + let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 3) + return LazyVGrid(columns: columns, spacing: 8) { + tile( + "stats.week".localized, value: UsageFormatters.tokens(summary.week), + delta: delta(summary.week, summary.previousWeek)) + tile( + "stats.month".localized, value: UsageFormatters.tokens(summary.month), + delta: delta(summary.month, summary.previousMonth)) + tile( + "stats.lifetime".localized, value: UsageFormatters.tokens(summary.lifetime), + caption: summary.since.map { + "stats.since".localized($0.formatted(.dateTime.month(.abbreviated).year().locale(locale))) + }) + tile( + "stats.peak".localized, value: summary.peak.map { UsageFormatters.tokens($0.tokens) } ?? "—", + caption: summary.peak.map { $0.date.formatted(.dateTime.day().month(.abbreviated).locale(locale)) }) + tile( + "stats.streak".localized, value: "stats.days_value".localized(String(summary.streak.current)), + caption: "stats.streak_best".localized(String(summary.streak.longest)), symbol: "flame.fill") + tile( + "stats.active_days".localized, value: String(summary.activeDays), + caption: summary.since.map { + "stats.since".localized($0.formatted(.dateTime.day().month(.abbreviated).locale(locale))) + }) + } + } + + private func delta(_ current: Int, _ previous: Int?) -> Double? { + guard let previous, previous > 0 else { return nil } + return Double(current - previous) / Double(previous) + } + + private func tile( + _ title: String, value: String, delta: Double? = nil, caption: String? = nil, symbol: String? = nil + ) -> some View { + VStack(alignment: .leading, spacing: 3) { + HStack(spacing: 3) { + if let symbol { + Image(systemName: symbol).font(.system(size: 8, weight: .semibold)).accessibilityHidden(true) + .foregroundStyle(accent) + } + Text(title).font(.system(size: 9, weight: .medium)).foregroundStyle(.secondary).lineLimit(1) + } + Text(value).font(.system(size: 15, weight: .bold, design: .rounded)).monospacedDigit() + .lineLimit(1).minimumScaleFactor(0.7) + .contentTransition(.numericText()) + if let delta { + Text((delta >= 0 ? "▲ " : "▼ ") + UsageFormatters.percentage(abs(delta) * 100)) + .font(.system(size: 8.5, weight: .semibold)).monospacedDigit() + .foregroundStyle(delta >= 0 ? Color.orange : Color.green) + .help("stats.vs_previous".localized) + } else if let caption { + Text(caption).font(.system(size: 8.5)).foregroundStyle(.tertiary).lineLimit(1) + } else { + Text(" ").font(.system(size: 8.5)) + } + } + .padding(.horizontal, 9).padding(.vertical, 8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(RoundedRectangle(cornerRadius: 8).fill(Color.primary.opacity(0.04))) + } + + private func dailyCard(_ summary: StatsSummary) -> some View { + card(.daily, "stats.daily.title".localized, symbol: "chart.bar.fill") { + VStack(alignment: .leading, spacing: 8) { + StatsBarsView( + days: summary.recent, typical: summary.typicalDay, providers: providers, locale: locale, + tint: accent) + legend(summary.legend) + } + } + } + + private func legend(_ items: [StatsSegment]) -> some View { + HStack(spacing: 10) { + ForEach(items) { item in + HStack(spacing: 4) { + RoundedRectangle(cornerRadius: 2) + .fill(StatsBarsView.color(item, providers: providers)) + .frame(width: 8, height: 8) + Text(segmentName(item.id)).font(.system(size: 9)).foregroundStyle(.secondary).lineLimit(1) + } + } + Spacer(minLength: 0) + } + } + + private func segmentName(_ id: String) -> String { + if id == StatsSummary.otherLabel { return "stats.other".localized } + if let provider = UsageProvider(rawValue: id) { return name(provider) } + return id + } + + private func activityCard(_ summary: StatsSummary) -> some View { + card(.activity, "stats.activity.title".localized, symbol: "bolt.fill") { + HStack(spacing: 8) { + if let turns = summary.turns, turns.todayCount > 0 { + tile( + "stats.activity.today".localized, + value: UsageFormatters.duration(Double(turns.todayMilliseconds) / 1000), + caption: "stats.activity.turns".localized(String(turns.todayCount))) + } + if let turns = summary.turns { + tile( + "stats.activity.longest".localized, + value: UsageFormatters.duration(Double(turns.longestMilliseconds) / 1000), + caption: "stats.activity.longest_hint".localized) + } + if let share = summary.cachedShare { + tile( + "stats.activity.cache".localized, value: UsageFormatters.percentage(share * 100), + caption: "stats.activity.cache_hint".localized) + } + } + } + } + + @ViewBuilder private func rankings(_ summary: StatsSummary) -> some View { + VStack(spacing: 10) { + if !summary.models.isEmpty { + card(.models, "stats.models.title".localized, symbol: "cpu") { rankList(summary.models, unit: nil) } + } + if !summary.efforts.isEmpty { + card(.efforts, "stats.efforts.title".localized, symbol: "brain.head.profile") { + rankList(summary.efforts, unit: nil) + } + } + if !summary.skills.isEmpty { + card(.skills, "stats.skills.title".localized, symbol: "wand.and.stars") { + rankList(summary.skills, unit: "stats.skills.runs") + } + } + } + } + + private func rankList(_ items: [StatsRankItem], unit: String?) -> some View { + let maximum = max(1, items.map(\.value).max() ?? 1) + let total = max(1, items.map(\.value).reduce(0, +)) + return VStack(spacing: 6) { + ForEach(Array(items.enumerated()), id: \.element.id) { index, item in + let tint = item.provider.map(color) ?? accent + HStack(spacing: 8) { + if let provider = item.provider, providers.count > 1 { + ProviderLogo(provider: provider, size: 10) + } + Text(item.label).font(.system(size: 10.5, weight: .medium)).lineLimit(1).truncationMode(.middle) + .frame(width: unit == nil ? 104 : 150, alignment: .leading) + .help(item.label) + GeometryReader { proxy in + ZStack(alignment: .leading) { + Capsule().fill(Color.primary.opacity(0.06)) + Capsule().fill(tint.opacity(1 - Double(index) * 0.12)) + .frame( + width: shown + ? max(4, proxy.size.width * CGFloat(item.value) / CGFloat(maximum)) : 4 + ) + .animation( + animates + ? .spring(response: 0.55, dampingFraction: 0.8).delay( + 0.25 + 0.04 * Double(index)) + : nil, value: appeared) + } + } + .frame(height: 6) + Text( + unit.map { (item.value == 1 ? $0 + "_one" : $0).localized(String(item.value)) } + ?? UsageFormatters.tokens(item.value) + ) + .font(.system(size: 9.5, weight: .semibold)).monospacedDigit() + .foregroundStyle(.secondary) + .frame(width: unit == nil ? 44 : 58, alignment: .trailing) + .help(unit == nil ? UsageFormatters.percentage(Double(item.value) * 100 / Double(total)) : "") + } + } + } + } + + private func color(_ provider: UsageProvider) -> Color { provider == .claude ? Theme.claude : Theme.codex } + private func name(_ provider: UsageProvider) -> String { + (provider == .claude ? "provider.claude_short" : "provider.codex").localized + } + +} + +struct PressableStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .scaleEffect(configuration.isPressed ? 0.97 : 1) + .animation(.easeOut(duration: 0.12), value: configuration.isPressed) + } +} diff --git a/Sources/TokenGaugeCapture/main.swift b/Sources/TokenGaugeCapture/main.swift index da86b14..72c3e7d 100644 --- a/Sources/TokenGaugeCapture/main.swift +++ b/Sources/TokenGaugeCapture/main.swift @@ -7,16 +7,18 @@ enum TokenGaugeCapture { let arguments = Set(CommandLine.arguments.dropFirst()) if arguments.contains("--collect") { let scan = try TranscriptScanner.scan(stateURL: TranscriptScanner.stateURL()) - let recorded = (try? UsageHistoryStore.recordEffort(scan.effortRecords)) != nil + let recorded = + (try? UsageHistoryStore.recordEffort(scan.effortRecords, activity: scan.activityRecords)) != nil try emit(CaptureCollection(buckets: scan.buckets, effortRecords: recorded ? scan.effortRecords.count : nil)) return } let recordEffort = arguments.contains("--record-effort-history") if recordEffort || arguments.contains("--effort-history") { do { - let records = try TranscriptScanner.scan(stateURL: TranscriptScanner.stateURL()).effortRecords + let scan = try TranscriptScanner.scan(stateURL: TranscriptScanner.stateURL()) + let records = scan.effortRecords if recordEffort { - try UsageHistoryStore.recordEffort(records) + try UsageHistoryStore.recordEffort(records, activity: scan.activityRecords) try emit(["records": records.count]) } else { try emit(records) diff --git a/Sources/TokenGaugeCore/ActivityUsageRecord.swift b/Sources/TokenGaugeCore/ActivityUsageRecord.swift new file mode 100644 index 0000000..8978c2b --- /dev/null +++ b/Sources/TokenGaugeCore/ActivityUsageRecord.swift @@ -0,0 +1,84 @@ +import Foundation + +public enum HistoryActivityKind: String, Codable, Sendable { + case skill, turn +} + +public struct ActivityUsageRecord: Equatable, Sendable { + public let id: String + public let provider: UsageProvider + public let recordedAt: Date + public let day: String + public let kind: HistoryActivityKind + public let key: String + public let value: Int + + public init( + id: String, provider: UsageProvider, recordedAt: Date, day: String, kind: HistoryActivityKind, key: String, + value: Int + ) { + self.id = id + self.provider = provider + self.recordedAt = recordedAt + self.day = day + self.kind = kind + self.key = key + self.value = value + } + + private static let skillCharacters = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyz0123456789._:-") + private static let skillLeadingCharacters = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyz0123456789") + + static func isSkillName(_ name: String) -> Bool { + guard let first = name.unicodeScalars.first, name.utf8.count <= 64, + skillLeadingCharacters.contains(first) + else { return false } + return name.unicodeScalars.allSatisfy(skillCharacters.contains) + } + + var isValid: Bool { + guard value >= 0 else { return false } + switch kind { + case .skill: guard Self.isSkillName(key) else { return false } + case .turn: guard key.isEmpty else { return false } + } + return EffortUsageRecord.isValid(id: id, recordedAt: recordedAt, day: day) + } +} + +public struct HistoryActivityRow: Equatable, Sendable { + public let day: String + public let provider: UsageProvider + public let kind: HistoryActivityKind + public let key: String + public let count: Int + public let total: Int + public let maximum: Int + + public init( + day: String, provider: UsageProvider, kind: HistoryActivityKind, key: String, count: Int, total: Int, + maximum: Int + ) { + self.day = day + self.provider = provider + self.kind = kind + self.key = key + self.count = count + self.total = total + self.maximum = maximum + } +} + +public struct HistoryCachedRow: Equatable, Sendable { + public let day: String + public let provider: UsageProvider + public let cachedTokens: Int + public let tokens: Int + + public init(day: String, provider: UsageProvider, cachedTokens: Int, tokens: Int) { + self.day = day + self.provider = provider + self.cachedTokens = cachedTokens + self.tokens = tokens + } +} diff --git a/Sources/TokenGaugeCore/ActivityUsageScanner.swift b/Sources/TokenGaugeCore/ActivityUsageScanner.swift new file mode 100644 index 0000000..0872c25 --- /dev/null +++ b/Sources/TokenGaugeCore/ActivityUsageScanner.swift @@ -0,0 +1,89 @@ +import Foundation + +enum ActivityUsageScanner { + private static let readCommands: Set = ["sed", "cat", "head", "nl", "less", "bat", "tail"] + private static let commandBoundaries = CharacterSet(charactersIn: "\"\n;|&(`{") + private static let skillPath = try! NSRegularExpression( + pattern: #"(? [ActivityEntry] { + let time = date.timeIntervalSince1970 + if metadata.type == "system", metadata.subtype == "turn_duration" { + guard !subagent, metadata.isSidechain != true, let uuid = metadata.uuid, !uuid.isEmpty, + let duration = metadata.durationMs, duration >= 0 + else { return [] } + return [ActivityEntry(id: hash(.claude, "turn:\(uuid)"), at: time, kind: .turn, key: "", value: duration)] + } + guard metadata.type == "assistant", let message = parser.skillCalls(line)?.message, + let messageID = message.id, !messageID.isEmpty + else { return [] } + return (message.content ?? []).compactMap { item in + guard item.type == "tool_use", item.name == "Skill", let id = item.id, !id.isEmpty, + let skill = item.skill, ActivityUsageRecord.isSkillName(skill) + else { return nil } + return ActivityEntry( + id: hash(.claude, "skill:\(messageID):\(id)"), at: time, kind: .skill, key: skill, value: 1) + } + } + + static func codex( + _ metadata: EffortTranscriptMetadata, line: Data, at date: Date, rollout: String, subagent: Bool, + parser: TranscriptLineParser + ) -> [ActivityEntry] { + let time = date.timeIntervalSince1970 + guard let payload = metadata.payload else { return [] } + switch (metadata.type, payload.type) { + case ("event_msg", "task_complete"): + guard !subagent, let turn = payload.turn_id, !turn.isEmpty, let duration = payload.duration_ms, + duration >= 0 + else { return [] } + return [ActivityEntry(id: hash(.codex, "turn:\(turn)"), at: time, kind: .turn, key: "", value: duration)] + case ("response_item", "function_call"), ("response_item", "custom_tool_call"): + guard let call = parser.toolCall(line)?.payload else { return [] } + return readSkills(in: call.arguments ?? call.input ?? "").sorted().map { skill in + ActivityEntry( + id: hash(.codex, "skill:\(rollout):\(skill)"), at: time, kind: .skill, key: skill, value: 1) + } + default: + return [] + } + } + + static func readSkills(in command: String) -> Set { + guard command.contains("SKILL.md") else { return [] } + let text = command.replacingOccurrences(of: "\\n", with: "\n") as NSString + var skills: Set = [] + for match in skillPath.matches(in: text as String, range: NSRange(location: 0, length: text.length)) { + let prefix = text.substring(to: match.range.location) + let start = prefix.rangeOfCharacter(from: commandBoundaries, options: .backwards)?.upperBound + let segment = start.map { prefix[$0...] } ?? prefix[...] + guard let program = segment.split(whereSeparator: \.isWhitespace).first?.split(separator: "/").last, + readCommands.contains(String(program)) + else { continue } + skills.insert(text.substring(with: match.range(at: 1))) + } + return skills + } + + static func records( + _ entries: some Sequence<(provider: UsageProvider, entry: ActivityEntry)>, cutoff: Date, now: Date, + calendar: Calendar + ) -> [ActivityUsageRecord] { + let earliest = cutoff.timeIntervalSince1970 + let latest = now.timeIntervalSince1970 + return entries.compactMap { provider, entry -> ActivityUsageRecord? in + guard entry.at >= earliest, entry.at <= latest else { return nil } + let recordedAt = Date(timeIntervalSince1970: entry.at) + return ActivityUsageRecord( + id: entry.id, provider: provider, recordedAt: recordedAt, + day: TranscriptScanner.dayString(recordedAt, calendar: calendar), kind: entry.kind, key: entry.key, + value: entry.value) + }.sorted { $0.recordedAt == $1.recordedAt ? $0.id < $1.id : $0.recordedAt < $1.recordedAt } + } + + private static func hash(_ provider: UsageProvider, _ identifier: String) -> String { + EffortUsageScanner.hash(provider: provider, identifier: identifier) + } +} diff --git a/Sources/TokenGaugeCore/ClaudeHistoryScanner.swift b/Sources/TokenGaugeCore/ClaudeHistoryScanner.swift index 98c1660..ffe6542 100644 --- a/Sources/TokenGaugeCore/ClaudeHistoryScanner.swift +++ b/Sources/TokenGaugeCore/ClaudeHistoryScanner.swift @@ -23,7 +23,7 @@ public enum ClaudeHistoryScanner { tokens: tokens, model: normalizedModel(message.model), effortModel: EffortTranscriptMetadata.normalizedModel(message.model), effort: EffortTranscriptMetadata.normalizedEffort(metadata.perTurnEffort ?? metadata.effort), - eligible: message.id?.isEmpty == false) + eligible: message.id?.isEmpty == false, cachedTokens: usage.cache_read_input_tokens.map { max(0, $0) }) } static func buckets( diff --git a/Sources/TokenGaugeCore/EffortTranscriptMetadata.swift b/Sources/TokenGaugeCore/EffortTranscriptMetadata.swift index 4ae3e75..1311a7f 100644 --- a/Sources/TokenGaugeCore/EffortTranscriptMetadata.swift +++ b/Sources/TokenGaugeCore/EffortTranscriptMetadata.swift @@ -4,13 +4,19 @@ struct EffortTranscriptMetadata: Decodable { let type: String? let timestamp: String? let uuid: String? + let subtype: String? + let durationMs: Int? + let isSidechain: Bool? let perTurnEffort: String? let effort: String? let payload: Payload? let message: Message? struct Payload: Decodable { + let type: String? let turn_id: String? + let duration_ms: Int? + let source: Source? let response_id: String? let model: String? let effort: String? @@ -29,6 +35,7 @@ struct EffortTranscriptMetadata: Decodable { let output_tokens: Int? let cache_creation_input_tokens: Int? let cache_read_input_tokens: Int? + let cached_input_tokens: Int? var claudeTotal: Int? { var total = 0 @@ -41,6 +48,57 @@ struct EffortTranscriptMetadata: Decodable { } } + struct Source: Decodable { + let isSubagent: Bool + + private enum CodingKeys: String, CodingKey { case subagent } + + init(from decoder: Decoder) throws { + if let value = try? decoder.singleValueContainer().decode(String.self) { + isSubagent = value == "subagent" + } else { + isSubagent = (try? decoder.container(keyedBy: CodingKeys.self).contains(.subagent)) == true + } + } + } + + struct SkillCalls: Decodable { + let message: Message? + + struct Message: Decodable { + let id: String? + let content: [Item]? + } + + struct Item: Decodable { + let type: String? + let id: String? + let name: String? + let skill: String? + + private enum CodingKeys: String, CodingKey { case type, id, name, input } + private enum InputKeys: String, CodingKey { case skill } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + type = try? container.decode(String.self, forKey: .type) + id = try? container.decode(String.self, forKey: .id) + name = try? container.decode(String.self, forKey: .name) + skill = try? container.nestedContainer(keyedBy: InputKeys.self, forKey: .input).decode( + String.self, forKey: .skill) + } + } + } + + struct ToolCall: Decodable { + let payload: Payload? + + struct Payload: Decodable { + let arguments: String? + let input: String? + } + } + private static let efforts: Set = [ "none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra", "auto", ] diff --git a/Sources/TokenGaugeCore/EffortUsageRecord.swift b/Sources/TokenGaugeCore/EffortUsageRecord.swift index a685f98..9a24147 100644 --- a/Sources/TokenGaugeCore/EffortUsageRecord.swift +++ b/Sources/TokenGaugeCore/EffortUsageRecord.swift @@ -8,9 +8,11 @@ public struct EffortUsageRecord: Codable, Equatable, Sendable { public let model: String public let effort: String public let tokens: Int + public let cachedTokens: Int? public init( - id: String, provider: UsageProvider, recordedAt: Date, day: String, model: String, effort: String, tokens: Int + id: String, provider: UsageProvider, recordedAt: Date, day: String, model: String, effort: String, tokens: Int, + cachedTokens: Int? = nil ) { self.id = id self.provider = provider @@ -19,6 +21,7 @@ public struct EffortUsageRecord: Codable, Equatable, Sendable { self.model = model self.effort = effort self.tokens = tokens + self.cachedTokens = cachedTokens } private static let hexCharacters = CharacterSet(charactersIn: "0123456789abcdefABCDEF") @@ -34,19 +37,24 @@ public struct EffortUsageRecord: Codable, Equatable, Sendable { }() var isValid: Bool { - guard id.utf8.count == 64, id.unicodeScalars.allSatisfy(Self.hexCharacters.contains), - !model.isEmpty, model.utf8.count <= 128, + guard !model.isEmpty, model.utf8.count <= 128, model.unicodeScalars.allSatisfy(Self.identifierCharacters.contains), - Self.efforts.contains(effort), tokens >= 0, + Self.efforts.contains(effort), tokens >= 0, (cachedTokens ?? 0) >= 0 + else { return false } + return Self.isValid(id: id, recordedAt: recordedAt, day: day) + } + + static func isValid(id: String, recordedAt: Date, day: String) -> Bool { + guard id.utf8.count == 64, id.unicodeScalars.allSatisfy(hexCharacters.contains), recordedAt.timeIntervalSince1970.isFinite, (0...253_402_300_799).contains(recordedAt.timeIntervalSince1970), - Self.isDayFormat(day) + isDayFormat(day) else { return false } let parts = day.split(separator: "-").compactMap { Int($0) } guard parts.count == 3, (1970...9999).contains(parts[0]) else { return false } let components = DateComponents(year: parts[0], month: parts[1], day: parts[2]) - guard let date = Self.utcCalendar.date(from: components) else { return false } - return Self.utcCalendar.dateComponents([.year, .month, .day], from: date) == components + guard let date = utcCalendar.date(from: components) else { return false } + return utcCalendar.dateComponents([.year, .month, .day], from: date) == components } private static func isDayFormat(_ day: String) -> Bool { diff --git a/Sources/TokenGaugeCore/EffortUsageScanner.swift b/Sources/TokenGaugeCore/EffortUsageScanner.swift index dcb6194..672c334 100644 --- a/Sources/TokenGaugeCore/EffortUsageScanner.swift +++ b/Sources/TokenGaugeCore/EffortUsageScanner.swift @@ -24,7 +24,8 @@ public enum EffortUsageScanner { let time = date.timeIntervalSince1970 return ScanEntry( id: hash(provider: .codex, identifier: identifier), firstAt: time, peakAt: time, tokens: tokens, - model: model, effortModel: model, effort: matching?.effort ?? "unknown", eligible: true) + model: model, effortModel: model, effort: matching?.effort ?? "unknown", eligible: true, + cachedTokens: payload.usage?.cached_input_tokens.map { max(0, $0) }) } static func records( @@ -39,7 +40,7 @@ public enum EffortUsageScanner { return EffortUsageRecord( id: entry.id, provider: provider, recordedAt: recordedAt, day: TranscriptScanner.dayString(recordedAt, calendar: calendar), - model: entry.effortModel, effort: entry.effort, tokens: entry.tokens) + model: entry.effortModel, effort: entry.effort, tokens: entry.tokens, cachedTokens: entry.cachedTokens) }.sorted { $0.recordedAt == $1.recordedAt ? $0.id < $1.id : $0.recordedAt < $1.recordedAt } } diff --git a/Sources/TokenGaugeCore/QuotaForecast.swift b/Sources/TokenGaugeCore/QuotaForecast.swift new file mode 100644 index 0000000..666a425 --- /dev/null +++ b/Sources/TokenGaugeCore/QuotaForecast.swift @@ -0,0 +1,201 @@ +import Foundation + +public struct ForecastPoint: Equatable, Sendable { + public let date: Date + public let remaining: Double + + public init(date: Date, remaining: Double) { + self.date = date + self.remaining = remaining + } +} + +public struct QuotaForecast: Equatable, Identifiable, Sendable { + public enum Verdict: Equatable, Sendable { + case noUsage + case lasts(margin: Double) + case tight(margin: Double) + case runsOut(at: Date) + case exhausted + } + + public let provider: UsageProvider + public let windowID: String + public let displayName: String? + public let start: Date + public let reset: Date + public let now: Date + public let points: [ForecastPoint] + public let remaining: Double + public let boosts: [ForecastPoint] + public let away: [DateInterval] + public let pointsPerHour: Double? + public let recentPointsPerHour: Double? + + public struct Horizon: Equatable, Sendable { + public let pace: TimeInterval + public let recent: TimeInterval + let minimumPace: TimeInterval + let minimumRecent: TimeInterval + let spansPreviousWindows: Bool + + public static let weekly = Horizon( + pace: 72 * 3600, recent: 24 * 3600, minimumPace: 3 * 3600, minimumRecent: 6 * 3600, + spansPreviousWindows: true) + public static let session = Horizon( + pace: 5 * 3600, recent: 3600, minimumPace: 10 * 60, minimumRecent: 30 * 60, spansPreviousWindows: false) + + public static func of(durationMinutes minutes: Int) -> Horizon { minutes < 1_440 ? .session : .weekly } + } + + public static let tightMargin = 10.0 + static let boostThreshold = 3.0 + static let resetTolerance: TimeInterval = 3600 + + public var id: String { provider.rawValue + ":" + windowID } + public var isSession: Bool { reset.timeIntervalSince(start) < 86_400 } + public var horizon: Horizon { isSession ? .session : .weekly } + public var budgetUnit: TimeInterval { isSession ? 3600 : 86_400 } + public var hoursLeft: Double { max(0, reset.timeIntervalSince(now) / 3600) } + + public var projectedRemainingAtReset: Double? { + pointsPerHour.map { remaining - $0 * hoursLeft } + } + + public var runsOutAt: Date? { exhaustion(pace: pointsPerHour) } + public var recentRunsOutAt: Date? { exhaustion(pace: recentPointsPerHour) } + + public var budget: Double { remaining / max(hoursLeft * 3600 / budgetUnit, 1) } + + public var verdict: Verdict { + if remaining <= 0 { return .exhausted } + guard let projected = projectedRemainingAtReset else { + return remaining >= 100 ? .noUsage : margin(remaining) + } + if projected <= 0, let date = runsOutAt { return .runsOut(at: date) } + if remaining >= 100, pointsPerHour == 0 { return .noUsage } + return margin(projected) + } + + private func margin(_ value: Double) -> Verdict { + value < Self.tightMargin ? .tight(margin: value) : .lasts(margin: value) + } + + public init( + provider: UsageProvider, windowID: String, displayName: String?, start: Date, reset: Date, now: Date, + points: [ForecastPoint], remaining: Double, boosts: [ForecastPoint], away: [DateInterval] = [], + pointsPerHour: Double?, recentPointsPerHour: Double? + ) { + self.provider = provider + self.windowID = windowID + self.displayName = displayName + self.start = start + self.reset = reset + self.now = now + self.points = points + self.remaining = remaining + self.boosts = boosts + self.away = away + self.pointsPerHour = pointsPerHour + self.recentPointsPerHour = recentPointsPerHour + } + + public static func make( + provider: UsageProvider, window: QuotaWindow, rows: [HistoryQuotaRow], now: Date, + accountFingerprint: String? = nil + ) -> QuotaForecast? { + guard let reset = window.resetsAt, let minutes = window.durationMinutes, minutes > 0, reset > now, + window.usedPercentage.isFinite + else { return nil } + let start = reset.addingTimeInterval(-Double(minutes) * 60) + guard start < now else { return nil } + let remaining = min(max(window.remainingPercentage, 0), 100) + let candidates = rows.filter { matches($0, provider: provider, window: window, now: now) } + .sorted { $0.sampledAt < $1.sampledAt } + let owns = { (row: HistoryQuotaRow) in + accountFingerprint == nil || row.accountFingerprint == nil || row.accountFingerprint == accountFingerprint + } + let matching = candidates.filter(owns) + let away = awayIntervals(candidates, owns: owns, now: now) + let observed = matching.filter { $0.sampledAt > start && sameReset($0.resetsAt, reset) }.map(point) + let origin = ForecastPoint(date: start, remaining: 100) + let current = ForecastPoint(date: now, remaining: remaining) + let points = [origin] + observed + [current] + let boosts = zip(points, points.dropFirst()).compactMap { previous, next in + next.remaining - previous.remaining > boostThreshold && !crosses(away, previous, next) ? next : nil + } + let horizon = Horizon.of(durationMinutes: minutes) + let previous = horizon.spansPreviousWindows ? matching.filter { $0.sampledAt < start }.map(point) : [] + let timeline = previous + points + return QuotaForecast( + provider: provider, windowID: window.id, displayName: window.displayName, start: start, reset: reset, + now: now, points: points, remaining: remaining, boosts: boosts, away: away, + pointsPerHour: pace(timeline, away: away, span: horizon.pace, minimum: horizon.minimumPace, now: now), + recentPointsPerHour: pace( + timeline, away: away, span: horizon.recent, minimum: horizon.minimumRecent, now: now)) + } + + private static func matches(_ row: HistoryQuotaRow, provider: UsageProvider, window: QuotaWindow, now: Date) -> Bool + { + row.provider == provider && row.windowID == window.id && row.sampledAt < now && row.usedPercentage.isFinite + && (0...100).contains(row.usedPercentage) + } + + private static func sameReset(_ rowReset: Date?, _ reset: Date) -> Bool { + rowReset.map { abs($0.timeIntervalSince(reset)) <= resetTolerance } ?? false + } + + private static func point(_ row: HistoryQuotaRow) -> ForecastPoint { + ForecastPoint(date: row.sampledAt, remaining: 100 - row.usedPercentage) + } + + static func pace( + _ timeline: [ForecastPoint], away: [DateInterval], span: TimeInterval, minimum: TimeInterval, now: Date + ) -> Double? { + guard let first = timeline.first else { return nil } + let cutoff = max(now.addingTimeInterval(-span), first.date) + let window = + [ForecastPoint(date: cutoff, remaining: interpolatedRemaining(timeline, at: cutoff))] + + timeline.filter { $0.date > cutoff } + let steps = zip(window, window.dropFirst()).filter { !crosses(away, $0.0, $0.1) } + let elapsed = steps.reduce(0) { $0 + $1.1.date.timeIntervalSince($1.0.date) } + guard elapsed >= minimum else { return nil } + let consumed = steps.reduce(0) { $0 + max(0, $1.0.remaining - $1.1.remaining) } + return consumed / (elapsed / 3600) + } + + public static func crosses(_ away: [DateInterval], _ from: ForecastPoint, _ to: ForecastPoint) -> Bool { + away.contains { $0.start < to.date && $0.end > from.date } + } + + static func awayIntervals(_ rows: [HistoryQuotaRow], owns: (HistoryQuotaRow) -> Bool, now: Date) -> [DateInterval] { + var intervals: [DateInterval] = [] + var leftAt: Date? + for row in rows { + if !owns(row) { + leftAt = leftAt ?? row.sampledAt + } else if let start = leftAt { + intervals.append(DateInterval(start: start, end: row.sampledAt)) + leftAt = nil + } + } + if let start = leftAt, start < now { intervals.append(DateInterval(start: start, end: now)) } + return intervals + } + + public static func interpolatedRemaining(_ points: [ForecastPoint], at date: Date) -> Double { + guard let index = points.firstIndex(where: { $0.date >= date }) else { return points.last?.remaining ?? 100 } + guard index > 0 else { return points[index].remaining } + let before = points[index - 1] + let after = points[index] + let interval = after.date.timeIntervalSince(before.date) + guard interval > 0 else { return after.remaining } + return before.remaining + (after.remaining - before.remaining) * date.timeIntervalSince(before.date) / interval + } + + private func exhaustion(pace: Double?) -> Date? { + guard let pace, pace > 0, remaining > 0 else { return nil } + let hours = remaining / pace + return hours < hoursLeft ? now.addingTimeInterval(hours * 3600) : nil + } +} diff --git a/Sources/TokenGaugeCore/ScanState.swift b/Sources/TokenGaugeCore/ScanState.swift index 3c59afd..a940c04 100644 --- a/Sources/TokenGaugeCore/ScanState.swift +++ b/Sources/TokenGaugeCore/ScanState.swift @@ -14,9 +14,12 @@ public enum TranscriptScanner { public struct Result: Sendable { public let buckets: [ModelTokenBucket] public let effortRecords: [EffortUsageRecord] + public let activityRecords: [ActivityUsageRecord] public let bytesRead: Int } + static let checkpointInterval: TimeInterval = 5 + public static func stateURL(homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser) -> URL { UsagePaths.supportDirectory(homeDirectory: homeDirectory).appending(path: "scan-state.json") } @@ -49,7 +52,7 @@ public enum TranscriptScanner { let historyStart = windowStart(days: historyDays, now: now, calendar: calendar) let effortStart = windowStart(days: effortDays, now: now, calendar: calendar) guard let readStart = [historyStart, effortStart].compactMap({ $0?.timeIntervalSince1970 }).min() else { - return Result(buckets: [], effortRecords: [], bytesRead: 0) + return Result(buckets: [], effortRecords: [], activityRecords: [], bytesRead: 0) } let loaded = stateURL.flatMap { ScanState.load(from: $0, windowStart: readStart) } let previous = loaded?.state @@ -77,6 +80,14 @@ public enum TranscriptScanner { let parser = TranscriptLineParser() var bytesRead = 0 var written = loaded?.encoded + var checkpointed = ProcessInfo.processInfo.systemUptime + func checkpoint() { + guard let stateURL else { return } + var partial = state + partial.files.merge(previous?.files ?? [:]) { current, _ in current } + persist(partial, to: stateURL, encoded: &written) + checkpointed = ProcessInfo.processInfo.systemUptime + } for root in roots { for (file, attributes) in candidateFiles(root: root.url, wanted: root.wanted) { let key = file.path @@ -85,14 +96,16 @@ public enum TranscriptScanner { parser: parser, prune: readStart) bytesRead += scanned.bytesRead state.files[key] = scanned.file + if ProcessInfo.processInfo.systemUptime - checkpointed >= checkpointInterval { checkpoint() } } - if let stateURL { persist(state, to: stateURL, encoded: &written) } + checkpoint() } let historyCutoff = historyStart?.timeIntervalSince1970 let effortCutoff = effortStart?.timeIntervalSince1970 var historyEntries: [String: ScanEntry] = [:] var effortEntries: [String: (provider: UsageProvider, entry: ScanEntry)] = [:] + var activityEntries: [String: (provider: UsageProvider, entry: ActivityEntry)] = [:] for key in state.files.keys.sorted() { guard let file = state.files[key] else { continue } let recent = recentCodexDirectories.contains(dayDirectory(URL(filePath: key).deletingLastPathComponent())) @@ -108,6 +121,10 @@ public enum TranscriptScanner { effortEntries[entry.id] = (file.provider, merged) } } + guard includeEffort else { continue } + for entry in file.activities { + activityEntries[entry.id] = (file.provider, activityEntries[entry.id]?.entry.merging(entry) ?? entry) + } } if let stateURL { persist(state, to: stateURL, encoded: &written) } @@ -118,6 +135,9 @@ public enum TranscriptScanner { effortRecords: effortStart.map { EffortUsageScanner.records(effortEntries.values, cutoff: $0, now: now, calendar: calendar) } ?? [], + activityRecords: effortStart.map { + ActivityUsageScanner.records(activityEntries.values, cutoff: $0, now: now, calendar: calendar) + } ?? [], bytesRead: bytesRead) } @@ -178,13 +198,18 @@ public enum TranscriptScanner { if let reusable, reusable.size == attributes.size, reusable.modified == attributes.modified { return (reusable, 0) } + let claudeSubagent = provider == .claude && url.pathComponents.contains("subagents") var entries: [String: ScanEntry] = [:] + var activities: [String: ActivityEntry] = [:] + var subagent = claudeSubagent var context: CodexTurnContext? var offset = 0 if let reusable, attributes.size > reusable.size, attributes.modified >= reusable.modified, ((try? JSONLReader.anchor(url, endingAt: reusable.offset)) ?? nil) == reusable.anchor { entries = Dictionary(reusable.entries.map { ($0.id, $0) }, uniquingKeysWith: { first, _ in first }) + activities = Dictionary(reusable.activities.map { ($0.id, $0) }, uniquingKeysWith: { first, _ in first }) + subagent = reusable.subagent context = reusable.context offset = reusable.offset } @@ -192,18 +217,31 @@ public enum TranscriptScanner { autoreleasepool { guard let metadata = parser.metadata(line) else { return } let entry: ScanEntry? + let found: [ActivityEntry] switch provider { case .claude: guard let date = parser.date(metadata.timestamp) else { return } + found = ActivityUsageScanner.claude( + metadata, line: line, at: date, subagent: subagent, parser: parser) entry = ClaudeHistoryScanner.entry(from: metadata, at: date) case .codex: if metadata.type == "turn_context" { context = metadata.payload.map(CodexTurnContext.init) return } + if metadata.type == "session_meta" { + subagent = metadata.payload?.source?.isSubagent == true + return + } guard let date = parser.date(metadata.timestamp) else { return } + found = ActivityUsageScanner.codex( + metadata, line: line, at: date, rollout: url.lastPathComponent, subagent: subagent, + parser: parser) entry = EffortUsageScanner.entry(from: metadata, at: date, context: context) } + for activity in found { + activities[activity.id] = activities[activity.id]?.merging(activity) ?? activity + } guard let entry else { return } entries[entry.id] = entries[entry.id]?.merging(entry) ?? entry } @@ -213,16 +251,21 @@ public enum TranscriptScanner { progress = try JSONLReader.read(url, from: offset, lineHandler: handler) } catch JSONLReader.ReadError.offsetNotAtLineBoundary { entries = [:] + activities = [:] + subagent = claudeSubagent context = nil progress = try JSONLReader.read(url, lineHandler: handler) } let kept = entries.values.filter { $0.peakAt >= prune }.sorted { $0.firstAt == $1.firstAt ? $0.id < $1.id : $0.firstAt < $1.firstAt } + let keptActivities = activities.values.filter { $0.at >= prune }.sorted { + $0.at == $1.at ? $0.id < $1.id : $0.at < $1.at + } let file = ScanFile( provider: provider, size: attributes.size, modified: attributes.modified, created: attributes.created, offset: progress.committedOffset, anchor: try JSONLReader.anchor(url, endingAt: progress.committedOffset), - context: context, entries: kept) + context: context, entries: kept, subagent: subagent, activities: keptActivities) return (file, progress.bytesRead) } } @@ -241,6 +284,19 @@ struct TranscriptLineParser { try? decoder.decode(EffortTranscriptMetadata.self, from: line) } + func skillCalls(_ line: Data) -> EffortTranscriptMetadata.SkillCalls? { + guard line.range(of: Self.skillMarker) != nil else { return nil } + return try? decoder.decode(EffortTranscriptMetadata.SkillCalls.self, from: line) + } + + func toolCall(_ line: Data) -> EffortTranscriptMetadata.ToolCall? { + guard line.range(of: Self.skillFileMarker) != nil else { return nil } + return try? decoder.decode(EffortTranscriptMetadata.ToolCall.self, from: line) + } + + private static let skillMarker = Data("\"Skill\"".utf8) + private static let skillFileMarker = Data("SKILL.md".utf8) + func date(_ timestamp: String?) -> Date? { guard let timestamp else { return nil } return fractional.date(from: timestamp) ?? standard.date(from: timestamp) @@ -248,7 +304,7 @@ struct TranscriptLineParser { } struct ScanState: Codable { - static let currentVersion = 2 + static let currentVersion = 3 var version: Int var windowStart: TimeInterval @@ -272,6 +328,23 @@ struct ScanFile: Codable { var anchor: String? var context: CodexTurnContext? var entries: [ScanEntry] + var subagent: Bool + var activities: [ActivityEntry] +} + +struct ActivityEntry: Codable, Equatable { + var id: String + var at: TimeInterval + var kind: HistoryActivityKind + var key: String + var value: Int + + func merging(_ incoming: ActivityEntry) -> ActivityEntry { + var merged = self + merged.at = min(at, incoming.at) + merged.value = max(value, incoming.value) + return merged + } } struct CodexTurnContext: Codable, Equatable { @@ -295,6 +368,7 @@ struct ScanEntry: Equatable { var effortModel: String var effort: String var eligible: Bool + var cachedTokens: Int? func merging(_ incoming: ScanEntry) -> ScanEntry { var merged = self @@ -307,6 +381,7 @@ struct ScanEntry: Equatable { if effortModel == "unknown" { merged.effortModel = incoming.effortModel } if effort == "unknown" { merged.effort = incoming.effort } merged.eligible = eligible || incoming.eligible + merged.cachedTokens = [cachedTokens, incoming.cachedTokens].compactMap { $0 }.max() return merged } } @@ -322,6 +397,7 @@ extension ScanEntry: Codable { effortModel = try container.decode(String.self) effort = try container.decode(String.self) eligible = try container.decode(Bool.self) + cachedTokens = try container.decodeIfPresent(Int.self) } func encode(to encoder: Encoder) throws { @@ -334,5 +410,6 @@ extension ScanEntry: Codable { try container.encode(effortModel) try container.encode(effort) try container.encode(eligible) + try container.encode(cachedTokens) } } diff --git a/Sources/TokenGaugeCore/UsageHistoryStore+Effort.swift b/Sources/TokenGaugeCore/UsageHistoryStore+Effort.swift index 92d347a..a6488c7 100644 --- a/Sources/TokenGaugeCore/UsageHistoryStore+Effort.swift +++ b/Sources/TokenGaugeCore/UsageHistoryStore+Effort.swift @@ -25,14 +25,17 @@ extension UsageHistoryStore { static func writeEffort(_ connection: SQLiteConnection, record: EffortUsageRecord) throws { let sql = """ - INSERT INTO effort_events (id, provider, occurred_at, day, model, effort, tokens) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) + INSERT INTO effort_events (id, provider, occurred_at, day, model, effort, tokens, cached_tokens) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) ON CONFLICT(id) DO UPDATE SET day = CASE WHEN excluded.occurred_at < effort_events.occurred_at THEN excluded.day ELSE effort_events.day END, occurred_at = MIN(effort_events.occurred_at, excluded.occurred_at), model = CASE WHEN effort_events.model = 'unknown' THEN excluded.model ELSE effort_events.model END, effort = CASE WHEN effort_events.effort = 'unknown' THEN excluded.effort ELSE effort_events.effort END, - tokens = MAX(effort_events.tokens, excluded.tokens) + tokens = MAX(effort_events.tokens, excluded.tokens), + cached_tokens = MAX( + COALESCE(effort_events.cached_tokens, excluded.cached_tokens), + COALESCE(excluded.cached_tokens, effort_events.cached_tokens)) WHERE effort_events.provider = excluded.provider; """ try connection.run(sql) { statement in @@ -43,6 +46,74 @@ extension UsageHistoryStore { statement.bind(5, record.model) statement.bind(6, record.effort) statement.bind(7, Int64(record.tokens)) + statement.bind(8, record.cachedTokens.map(Int64.init)) + } + } + + static func writeActivity(_ connection: SQLiteConnection, record: ActivityUsageRecord) throws { + let sql = """ + INSERT INTO activity_events (id, provider, occurred_at, day, kind, key, value) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) + ON CONFLICT(id) DO UPDATE SET + day = CASE WHEN excluded.occurred_at < activity_events.occurred_at THEN excluded.day ELSE activity_events.day END, + occurred_at = MIN(activity_events.occurred_at, excluded.occurred_at), + value = MAX(activity_events.value, excluded.value) + WHERE activity_events.provider = excluded.provider AND activity_events.kind = excluded.kind; + """ + try connection.run(sql) { statement in + statement.bind(1, record.id.lowercased()) + statement.bind(2, record.provider.rawValue) + statement.bind(3, record.recordedAt.timeIntervalSince1970) + statement.bind(4, record.day) + statement.bind(5, record.kind.rawValue) + statement.bind(6, record.key) + statement.bind(7, Int64(record.value)) } } + + static func readActivityRows( + _ connection: SQLiteConnection, since day: String?, through lastDay: String? + ) throws -> [HistoryActivityRow] { + let sql = """ + SELECT day, provider, kind, key, COUNT(*), SUM(value), MAX(value) FROM activity_events + WHERE day >= ?1 AND day <= ?2 + GROUP BY day, provider, kind, key ORDER BY day, provider, kind, key; + """ + var rows: [HistoryActivityRow] = [] + try connection.query(sql) { statement in + statement.bind(1, day ?? "0000-01-01") + statement.bind(2, lastDay ?? "9999-12-31") + } each: { statement in + guard let provider = UsageProvider(rawValue: statement.text(1)), + let kind = HistoryActivityKind(rawValue: statement.text(2)) + else { return } + rows.append( + HistoryActivityRow( + day: statement.text(0), provider: provider, kind: kind, key: statement.text(3), + count: statement.int(4), total: statement.int(5), maximum: statement.int(6))) + } + return rows + } + + static func readCachedRows( + _ connection: SQLiteConnection, since day: String?, through lastDay: String? + ) throws -> [HistoryCachedRow] { + let sql = """ + SELECT day, provider, SUM(cached_tokens), SUM(tokens) FROM effort_events + WHERE cached_tokens IS NOT NULL AND day >= ?1 AND day <= ?2 + GROUP BY day, provider ORDER BY day, provider; + """ + var rows: [HistoryCachedRow] = [] + try connection.query(sql) { statement in + statement.bind(1, day ?? "0000-01-01") + statement.bind(2, lastDay ?? "9999-12-31") + } each: { statement in + guard let provider = UsageProvider(rawValue: statement.text(1)) else { return } + rows.append( + HistoryCachedRow( + day: statement.text(0), provider: provider, cachedTokens: statement.int(2), + tokens: statement.int(3))) + } + return rows + } } diff --git a/Sources/TokenGaugeCore/UsageHistoryStore+Schema.swift b/Sources/TokenGaugeCore/UsageHistoryStore+Schema.swift index 167b6f8..f1e97f5 100644 --- a/Sources/TokenGaugeCore/UsageHistoryStore+Schema.swift +++ b/Sources/TokenGaugeCore/UsageHistoryStore+Schema.swift @@ -43,6 +43,16 @@ extension UsageHistoryStore { tokens INTEGER NOT NULL ) WITHOUT ROWID; CREATE INDEX IF NOT EXISTS effort_events_day_provider ON effort_events(day, provider); + CREATE TABLE IF NOT EXISTS activity_events ( + id TEXT NOT NULL PRIMARY KEY, + provider TEXT NOT NULL, + occurred_at REAL NOT NULL, + day TEXT NOT NULL, + kind TEXT NOT NULL, + key TEXT NOT NULL, + value INTEGER NOT NULL + ) WITHOUT ROWID; + CREATE INDEX IF NOT EXISTS activity_events_day_provider ON activity_events(day, provider); """ static let observedTotalsViews = """ @@ -106,6 +116,13 @@ extension UsageHistoryStore { if !paceColumns.contains("account_fingerprint") { try connection.exec("ALTER TABLE pace_samples ADD COLUMN account_fingerprint TEXT;") } + var effortColumns = Set() + try connection.query("PRAGMA table_info(effort_events);", bindings: { _ in }) { statement in + effortColumns.insert(statement.text(1)) + } + if !effortColumns.contains("cached_tokens") { + try connection.exec("ALTER TABLE effort_events ADD COLUMN cached_tokens INTEGER;") + } if try schemaVersion(connection) < Schema.version { try connection.exec(Schema.observedTotalsViews) try connection.exec("PRAGMA user_version = \(Schema.version);") diff --git a/Sources/TokenGaugeCore/UsageHistoryStore.swift b/Sources/TokenGaugeCore/UsageHistoryStore.swift index ae3f6d0..e0f6f37 100644 --- a/Sources/TokenGaugeCore/UsageHistoryStore.swift +++ b/Sources/TokenGaugeCore/UsageHistoryStore.swift @@ -86,17 +86,35 @@ public enum UsageHistoryStore { } public static func recordEffort( - _ records: [EffortUsageRecord], at url: URL = UsagePaths.history() + _ records: [EffortUsageRecord], activity: [ActivityUsageRecord] = [], at url: URL = UsagePaths.history() ) throws { let valid = records.filter(\.isValid) - guard !valid.isEmpty else { return } + let validActivity = activity.filter(\.isValid) + guard !valid.isEmpty || !validActivity.isEmpty else { return } try write(url) { connection in try connection.transaction { for record in valid { try writeEffort(connection, record: record) } + for record in validActivity { try writeActivity(connection, record: record) } } } } + public static func activityRows( + since day: String? = nil, through lastDay: String? = nil, at url: URL = UsagePaths.history() + ) throws -> [HistoryActivityRow] { + try read(url) { connection in + try readActivityRows(connection, since: day, through: lastDay) + } + } + + public static func cachedTokenRows( + since day: String? = nil, through lastDay: String? = nil, at url: URL = UsagePaths.history() + ) throws -> [HistoryCachedRow] { + try read(url) { connection in + try readCachedRows(connection, since: day, through: lastDay) + } + } + public static func effortRows( since day: String? = nil, through lastDay: String? = nil, at url: URL = UsagePaths.history() ) throws -> [HistoryEffortRow] { diff --git a/Tests/TokenGaugeAppTests/AppPreferencesTests.swift b/Tests/TokenGaugeAppTests/AppPreferencesTests.swift index 3f91094..29f9d58 100644 --- a/Tests/TokenGaugeAppTests/AppPreferencesTests.swift +++ b/Tests/TokenGaugeAppTests/AppPreferencesTests.swift @@ -17,6 +17,7 @@ final class AppPreferencesTests: XCTestCase { XCTAssertEqual(AppPreferences.Key.showHourlyPace, "showHourlyPace") XCTAssertEqual(AppPreferences.Key.showLunaReserve, "showLunaReserve") XCTAssertEqual(AppPreferences.Key.hiddenClaudeWindows, "hiddenClaudeWindows") + XCTAssertEqual(AppPreferences.Key.collapsedStatsCards, "collapsedStatsCards") XCTAssertEqual(AppPreferences.Key.claudeMenuBarSource, "claudeMenuBarSource") XCTAssertEqual(AppPreferences.Key.claudeAutomaticRecovery, "claudeAutomaticRecovery") XCTAssertEqual(AppPreferences.Key.claudeRecoveryNextAttempt, "claudeRecoveryNextAttempt") @@ -29,6 +30,18 @@ final class AppPreferencesTests: XCTestCase { XCTAssertEqual(AppPreferences.Key.cancellationDailyBaseline(.codex), "codexCancellationDailyBaseline") } + func testCollapsedStatsCardsPersistAndIgnoreUnknownValues() { + withDefaults { defaults in + let store = UsageStore(defaults: defaults) + store.toggleStatsCard(.models) + store.toggleStatsCard(.efforts) + store.toggleStatsCard(.models) + XCTAssertEqual(UsageStore(defaults: defaults).collapsedStatsCards, [.efforts]) + defaults.set(["bogus", "skills"], forKey: "collapsedStatsCards") + XCTAssertEqual(UsageStore(defaults: defaults).collapsedStatsCards, [.skills]) + } + } + func testLegacyDefaultsAreReadAndWrittenWithTheSameShapes() { withDefaults { defaults in let cancelledAt = Date(timeIntervalSince1970: 1_800_000_000) diff --git a/Tests/TokenGaugeAppTests/ModelActivityTests.swift b/Tests/TokenGaugeAppTests/ModelActivityTests.swift index d97f96d..b57875f 100644 --- a/Tests/TokenGaugeAppTests/ModelActivityTests.swift +++ b/Tests/TokenGaugeAppTests/ModelActivityTests.swift @@ -17,20 +17,20 @@ final class ModelActivityTests: XCTestCase { excludingFamilies: ["fable"] ) - XCTAssertEqual(chips.map(\.displayName), ["Opus", "Sonnet"]) + XCTAssertEqual(chips.map(\.displayName), ["Opus 5", "Sonnet 5"]) } func testScopedChipsStillReportTheirOwnFamily() { let chips = ModelActivity.chips(buckets: buckets, since: start, limit: 2, family: "Fable") - XCTAssertEqual(chips.map(\.displayName), ["Fable"]) + XCTAssertEqual(chips.map(\.displayName), ["Fable 5"]) XCTAssertEqual(chips.map(\.tokens), [78_800_000]) } func testNoExclusionKeepsEveryFamily() { let chips = ModelActivity.chips(buckets: buckets, since: start, limit: 3) - XCTAssertEqual(chips.map(\.displayName), ["Fable", "Opus", "Sonnet"]) + XCTAssertEqual(chips.map(\.displayName), ["Fable 5", "Opus 5", "Sonnet 5"]) } private var buckets: [ModelTokenBucket] { diff --git a/Tests/TokenGaugeAppTests/PopoverLayoutTests.swift b/Tests/TokenGaugeAppTests/PopoverLayoutTests.swift index 12160a3..d68598d 100644 --- a/Tests/TokenGaugeAppTests/PopoverLayoutTests.swift +++ b/Tests/TokenGaugeAppTests/PopoverLayoutTests.swift @@ -100,6 +100,34 @@ final class PopoverLayoutTests: XCTestCase { } } + func testPanelKeepsItsBottomPaddingInEveryMode() throws { + let suite = "TokenGauge.layout.\(UUID().uuidString)" + let defaults = try XCTUnwrap(UserDefaults(suiteName: suite)) + defer { defaults.removePersistentDomain(forName: suite) } + let store = UsageStore( + defaults: defaults, initialSnapshots: [codexSnapshot(), claudeSnapshot()], historyReadsEnabled: false) + for mode in UsageDisplayMode.allCases { + for history in [HistoryMode.recent, .calendar] { + store.displayMode = mode + store.historyMode = history + let bitmap = try render(PopoverView(store: store, showSettings: {}, showAbout: {})) + let scale = CGFloat(bitmap.pixelsHigh) / bitmap.size.height + let rows = Int((Theme.Layout.panelBottomPadding - 2) * scale) + let reference = try XCTUnwrap( + bitmap.colorAt(x: bitmap.pixelsWide / 2, y: bitmap.pixelsHigh - 1)?.usingColorSpace(.sRGB)) + let touched = ((bitmap.pixelsHigh - rows).. 0.05 + } + } + XCTAssertFalse(touched, "\(mode.rawValue) / \(history.rawValue)") + } + } + } + private func claudeSnapshot() -> ProviderUsageSnapshot { Fixture.snapshot( .claude, diff --git a/Tests/TokenGaugeAppTests/ReadmeScreenshotTests.swift b/Tests/TokenGaugeAppTests/ReadmeScreenshotTests.swift index 17ee2b7..8b2dc41 100644 --- a/Tests/TokenGaugeAppTests/ReadmeScreenshotTests.swift +++ b/Tests/TokenGaugeAppTests/ReadmeScreenshotTests.swift @@ -68,4 +68,102 @@ final class ReadmeScreenshotTests: XCTestCase { PopoverView(store: store, showSettings: {}, showAbout: {}, history: history), to: output.appendingPathComponent("readme-year.png")) } + + func testExportStats() throws { + guard let directory = ProcessInfo.processInfo.environment["TOKENGAUGE_SCREENSHOT_DIR"] else { + throw XCTSkip("Set TOKENGAUGE_SCREENSHOT_DIR to export the README stats") + } + let output = URL(fileURLWithPath: directory) + try FileManager.default.createDirectory(at: output, withIntermediateDirectories: true) + let language = LocalizationManager.shared.language + let suite = "TokenGauge.readme.\(UUID().uuidString)" + let defaults = try XCTUnwrap(UserDefaults(suiteName: suite)) + defer { + defaults.removePersistentDomain(forName: suite) + LocalizationManager.shared.language = language + } + LocalizationManager.shared.language = .english + let calendar = Calendar.current + let now = Date() + let today = calendar.startOfDay(for: now) + let weeklyReset = today.addingTimeInterval(2 * 86_400 + 10 * 3600) + let sessionReset = now.addingTimeInterval(2 * 3600 + 40 * 60) + var days: [HistoryCalendarDay] = [] + var tokens: [HistoryTokenRow] = [] + var efforts: [HistoryEffortRow] = [] + var activity: [HistoryActivityRow] = [] + var cached: [HistoryCachedRow] = [] + let efforts4 = ["high", "xhigh", "medium", "max"] + let skills = ["code-review", "release-notes", "test-runner", "docs-writer", "ui-polish"] + for offset in -64...0 { + let date = calendar.date(byAdding: .day, value: offset, to: today)! + let key = HistoryDashboardModel.dayKey(date) + let index = offset + 64 + let weekend = calendar.isDateInWeekend(date) + let base = ((index * 37 + 13) % 23 + 8) * (weekend ? 4_000_000 : 11_000_000) + let total = offset == 0 ? 412_000_000 : base + let split = [ + ("claude-opus-5-5", total * 62 / 100), ("claude-fable-5-1", total * 24 / 100), + ("claude-sonnet-5", total * 11 / 100), ("claude-haiku-4-5", total * 3 / 100), + ] + let rows = split.map { HistoryTokenRow(day: key, provider: .claude, model: $0.0, tokens: $0.1) } + let effortRows = efforts4.enumerated().map { position, effort in + HistoryEffortRow( + day: key, provider: .claude, model: "claude-opus-5-5", effort: effort, + tokens: total * [40, 27, 21, 12][position] / 100) + } + days.append(HistoryCalendarDay(id: key, date: date, totals: [.claude: total], efforts: effortRows)) + tokens += rows + efforts += effortRows + cached.append(HistoryCachedRow(day: key, provider: .claude, cachedTokens: total * 93 / 100, tokens: total)) + for (position, skill) in skills.enumerated() where (index + position) % (position + 2) == 0 { + activity.append( + HistoryActivityRow( + day: key, provider: .claude, kind: .skill, key: skill, count: 5 - position, + total: 5 - position, maximum: 1)) + } + activity.append( + HistoryActivityRow( + day: key, provider: .claude, kind: .turn, key: "", count: 18, + total: (38 + index % 20) * 60_000, maximum: (9 + index % 14) * 60_000)) + } + func quota(_ id: String, minutes: Int, reset: Date, samples: [(TimeInterval, Double)]) -> [HistoryQuotaRow] { + samples.map { + HistoryQuotaRow( + sampledAt: now.addingTimeInterval($0.0), provider: .claude, windowID: id, displayName: nil, + usedPercentage: $0.1, resetsAt: reset, durationMinutes: minutes, accountFingerprint: nil) + } + } + let weeklyStart = weeklyReset.addingTimeInterval(-7 * 86_400) + var used = 0.0 + let weeklySamples = stride(from: weeklyStart.timeIntervalSince(now) + 1800, to: 0, by: 1800).map { + offset -> (TimeInterval, Double) in + let hour = calendar.component(.hour, from: now.addingTimeInterval(offset)) + used += (10...23).contains(hour) ? 0.36 : 0.02 + return (offset, used) + } + let sessionSamples = stride(from: -2.3 * 3600, to: 0, by: 900).map { ($0, (2.3 + $0 / 3600) * 7.4) } + let records = StatsModel.Records( + input: StatsSummary.Input( + days: days, tokens: tokens, efforts: efforts, activity: activity, cached: cached), + quota: quota("seven_day", minutes: 10_080, reset: weeklyReset, samples: weeklySamples) + + quota("five_hour", minutes: 300, reset: sessionReset, samples: sessionSamples)) + let windows = [ + Fixture.window(id: "five_hour", usedPercentage: 17, resetsAt: sessionReset, durationMinutes: 300), + Fixture.window( + id: "seven_day", usedPercentage: weeklySamples.last?.1 ?? 40, resetsAt: weeklyReset, + durationMinutes: 10_080), + ] + let store = Fixture.store( + defaults: defaults, snapshots: [Fixture.snapshot(.claude, windows: windows, capturedAt: now)]) + store.displayMode = .claude + store.animateChanges = false + for kind in QuotaWindowKind.allCases { store.setClaudeWindow(kind, visible: true) } + let view = StatsView(store: store, model: StatsModel(preview: records), providers: [.claude], scrolls: false) + .frame(width: Theme.Layout.panelWidth - Theme.Layout.panelPadding * 2) + .padding(Theme.Layout.panelPadding) + .background(Theme.panelBackground) + .environment(\.quotaAnimationsEnabled, false) + try render(view, to: output.appendingPathComponent("stats.png"), maximumHeight: 2400) + } } diff --git a/Tests/TokenGaugeAppTests/StatsSummaryTests.swift b/Tests/TokenGaugeAppTests/StatsSummaryTests.swift new file mode 100644 index 0000000..0441549 --- /dev/null +++ b/Tests/TokenGaugeAppTests/StatsSummaryTests.swift @@ -0,0 +1,75 @@ +import TokenGaugeCore +import XCTest + +@testable import TokenGaugeApp + +@MainActor +final class StatsSummaryTests: XCTestCase { + private let calendar = Calendar.current + private var now: Date { calendar.date(bySettingHour: 16, minute: 0, second: 0, of: Date()) ?? Date() } + + private func input(days range: ClosedRange, today: Int, extraModel: String? = nil) -> StatsSummary.Input { + let start = calendar.startOfDay(for: now) + var days: [HistoryCalendarDay] = [] + var tokens: [HistoryTokenRow] = [] + for offset in range { + let date = calendar.date(byAdding: .day, value: offset, to: start) ?? start + let key = HistoryDashboardModel.dayKey(date) + let total = offset == 0 ? today : 100 + days.append(HistoryCalendarDay(id: key, date: date, totals: [.claude: total], efforts: [])) + tokens.append(HistoryTokenRow(day: key, provider: .claude, model: "claude-opus-5-5", tokens: total)) + } + if let extraModel { + tokens.append( + HistoryTokenRow( + day: HistoryDashboardModel.dayKey(start), provider: .claude, model: extraModel, tokens: 1)) + } + return StatsSummary.Input(days: days, tokens: tokens, efforts: []) + } + + func testSpikeAgainstTypicalDay() { + let summary = StatsSummary.make(input(days: -40...0, today: 300), providers: [.claude], now: now) + XCTAssertEqual(summary.typicalDay, 100) + XCTAssertEqual(summary.today, 300) + guard case .spike(let ratio) = summary.insight else { return XCTFail("\(String(describing: summary.insight))") } + XCTAssertEqual(ratio, 3, accuracy: 0.001) + } + + func testNewModelWinsOverSpike() { + let summary = StatsSummary.make( + input(days: -40...0, today: 300, extraModel: "claude-fable-5-1"), providers: [.claude], now: now) + guard case .newModel(let name) = summary.insight else { + return XCTFail("\(String(describing: summary.insight))") + } + XCTAssertEqual(name, "Fable 5.1") + } + + func testDeltasNeedFullPreviousPeriod() { + let short = StatsSummary.make(input(days: -2...0, today: 100), providers: [.claude], now: now) + XCTAssertNil(short.previousWeek) + XCTAssertNil(short.previousMonth) + XCTAssertNil(short.typicalDay) + let long = StatsSummary.make(input(days: -70...0, today: 100), providers: [.claude], now: now) + XCTAssertNotNil(long.previousWeek) + XCTAssertNotNil(long.previousMonth) + XCTAssertEqual(long.previousWeek, long.week) + } + + func testSkillsAndTurnsFollowSelectedProviders() { + var data = input(days: -10...0, today: 100) + let today = HistoryDashboardModel.dayKey(now) + data.activity = [ + HistoryActivityRow( + day: today, provider: .claude, kind: .skill, key: "memory-closeout", count: 3, total: 3, maximum: 1), + HistoryActivityRow( + day: today, provider: .claude, kind: .skill, key: "iabrain-board", count: 5, total: 5, maximum: 1), + HistoryActivityRow( + day: today, provider: .codex, kind: .skill, key: "agent-delegation", count: 9, total: 9, maximum: 1), + HistoryActivityRow( + day: today, provider: .claude, kind: .turn, key: "", count: 2, total: 90_000, maximum: 60_000), + ] + let summary = StatsSummary.make(data, providers: [.claude], now: now) + XCTAssertEqual(summary.skills.map(\.label), ["iabrain-board", "memory-closeout"]) + XCTAssertEqual(summary.turns, StatsTurns(todayMilliseconds: 90_000, todayCount: 2, longestMilliseconds: 60_000)) + } +} diff --git a/Tests/TokenGaugeAppTests/WindowVisibilityTests.swift b/Tests/TokenGaugeAppTests/WindowVisibilityTests.swift index cb85753..0a98ecf 100644 --- a/Tests/TokenGaugeAppTests/WindowVisibilityTests.swift +++ b/Tests/TokenGaugeAppTests/WindowVisibilityTests.swift @@ -37,10 +37,20 @@ final class WindowVisibilityTests: XCTestCase { } @MainActor - func testModelDisplayNamesCollapseToFamilies() { - XCTAssertEqual(ModelActivity.displayName("claude-fable-5"), "Fable") - XCTAssertEqual(ModelActivity.displayName("claude-opus-5[1m]"), "Opus") - XCTAssertEqual(ModelActivity.displayName("claude-haiku-4-5-20251001"), "Haiku") + func testModelFamiliesCollapseVersions() { + XCTAssertEqual(ModelActivity.family("claude-fable-5"), "Fable") + XCTAssertEqual(ModelActivity.family("claude-opus-5[1m]"), "Opus") + XCTAssertEqual(ModelActivity.family("claude-haiku-4-5-20251001"), "Haiku") + } + + @MainActor + func testModelDisplayNamesKeepTheVersion() { + XCTAssertEqual(ModelActivity.displayName("claude-opus-5-5"), "Opus 5.5") + XCTAssertEqual(ModelActivity.displayName("claude-opus-5[1m]"), "Opus 5") + XCTAssertEqual(ModelActivity.displayName("claude-fable-5-1"), "Fable 5.1") + XCTAssertEqual(ModelActivity.displayName("claude-haiku-4-5-20251001"), "Haiku 4.5") + XCTAssertEqual(ModelActivity.displayName("gpt-6-astra"), "GPT-6 Astra") + XCTAssertEqual(ModelActivity.displayName("gpt-5.3-codex"), "GPT-5.3 Codex") } @MainActor @@ -54,7 +64,7 @@ final class WindowVisibilityTests: XCTestCase { let chips = ModelActivity.chips(buckets: buckets, since: start) - XCTAssertEqual(chips.map(\.displayName), ["Opus", "Fable"]) + XCTAssertEqual(chips.map(\.displayName), ["Opus 5", "Fable 5"]) XCTAssertEqual(chips.map(\.tokens), [400, 100]) } diff --git a/Tests/TokenGaugeCoreTests/ActivityUsageScannerTests.swift b/Tests/TokenGaugeCoreTests/ActivityUsageScannerTests.swift new file mode 100644 index 0000000..b3cf7ef --- /dev/null +++ b/Tests/TokenGaugeCoreTests/ActivityUsageScannerTests.swift @@ -0,0 +1,260 @@ +import Foundation +import SQLite3 +import XCTest + +@testable import TokenGaugeCore + +final class ActivityUsageScannerTests: XCTestCase { + private let now = ISO8601DateFormatter().date(from: "2026-09-12T18:00:00Z")! + + func testClaudeSkillToolUseCountsButSkillTextDoesNot() throws { + try withHome { home in + try write( + home, ".claude/projects/project/a.jsonl", + [ + claudeSkill("m1", tool: "t1", skill: "memory-closeout"), + claudeSkill("m1", tool: "t1", skill: "memory-closeout"), + claudeSkill("m2", tool: "t2", skill: "anthropic-skills:pdf"), + claudeSkill("m3", tool: "t3", skill: "Bad Name"), + #""" + {"type":"assistant","timestamp":"2026-09-12T12:00:00.000Z","message":{"id":"m4","content":[{"type":"text","text":"call {\"type\":\"tool_use\",\"name\":\"Skill\",\"input\":{\"skill\":\"leak\"}}"}]}} + """#, + #""" + {"type":"user","timestamp":"2026-09-12T12:00:00.000Z","message":{"id":"m5","content":[{"type":"tool_use","id":"t5","name":"Skill","input":{"skill":"user-text"}}]}} + """#, + ]) + let records = try scan(home).activityRecords + XCTAssertEqual(records.map(\.key).sorted(), ["anthropic-skills:pdf", "memory-closeout"]) + XCTAssertTrue(records.allSatisfy { $0.kind == .skill && $0.value == 1 && $0.id.count == 64 }) + } + } + + func testCodexSkillReadCountsOncePerRolloutAndIgnoresSearchesAndEdits() throws { + try withHome { home in + let read = codexCall( + #"text(await tools.exec_command({cmd:\"cat /u/.agents/skills/memory-closeout/SKILL.md\"}))"#) + try write( + home, ".codex/sessions/2026/09/12/rollout-a.jsonl", + [ + sessionMeta(#""cli""#), read, read, + codexCall(#"tools.exec_command({cmd:\"sed -n '1,80p' /u/skills/memory-closeout/SKILL.md\"})"#), + codexCall(#"tools.exec_command({cmd:\"rg -n 'a|b' /u/skills/search-only/SKILL.md\"})"#), + codexCall(#"tools.exec_command({cmd:\"rg skills/grep-only/SKILL.md\"})"#), + codexCall( + #"*** Begin Patch\n*** Update File: /u/skills/patched/SKILL.md\n@@\n-a\n+b\n*** End Patch"#, + name: "apply_patch"), + codexCall(#"tools.exec_command({cmd:\"apply_patch skills/patched-cmd/SKILL.md\"})"#), + #""" + {"type":"response_item","timestamp":"2026-09-12T12:00:00Z","payload":{"type":"function_call","name":"exec_command","arguments":"{\"cmd\":\"cd /u && head -n 5 skills/agent-delegation/SKILL.md\"}"}} + """#, + ]) + try write(home, ".codex/sessions/2026/09/12/rollout-b.jsonl", [sessionMeta(#""cli""#), read]) + let records = try scan(home).activityRecords + XCTAssertEqual( + records.map(\.key).sorted(), ["agent-delegation", "memory-closeout", "memory-closeout"]) + XCTAssertEqual(Set(records.map(\.id)).count, 3) + } + } + + func testSubagentAndSidechainTurnsAreSkipped() throws { + try withHome { home in + try write( + home, ".claude/projects/project/a.jsonl", + [claudeTurn("u1", duration: 5000), claudeTurn("u2", duration: 9000, sidechain: true)]) + try write( + home, ".claude/projects/project/session/subagents/agent.jsonl", [claudeTurn("u3", duration: 7000)]) + try write( + home, ".codex/sessions/2026/09/12/rollout-main.jsonl", + [sessionMeta(#""vscode""#), codexTurn("turn-main", duration: 3000)]) + try write( + home, ".codex/sessions/2026/09/12/rollout-child.jsonl", + [sessionMeta(#"{"subagent":{"thread_spawn":{}}}"#), codexTurn("turn-child", duration: 4000)]) + try write( + home, ".codex/sessions/2026/09/12/rollout-legacy.jsonl", + [sessionMeta(#""subagent""#), codexTurn("turn-legacy", duration: 4000)]) + let records = try scan(home).activityRecords + XCTAssertEqual(records.map(\.value).sorted(), [3000, 5000]) + XCTAssertTrue(records.allSatisfy { $0.kind == .turn && $0.key.isEmpty }) + } + } + + func testRepeatedScansKeepActivityAndEffortTotalsIdentical() throws { + try withHome { home in + let state = home.appending(path: "support/scan-state.json") + let database = home.appending(path: "support/history.sqlite") + try write( + home, ".claude/projects/project/a.jsonl", + [ + claudeSkill("m1", tool: "t1", skill: "memory-closeout"), claudeTurn("u1", duration: 5000), + claudeUsage("m2", cached: 30), + ]) + try write( + home, ".codex/sessions/2026/09/12/rollout-a.jsonl", + [ + sessionMeta(#""cli""#), codexCall(#"tools.exec_command({cmd:\"cat skills/x/SKILL.md\"})"#), + codexTurn("turn-a", duration: 3000), codexUsage("r1", total: 100, cached: 60), + ]) + let first = try scan(home, state: state) + try UsageHistoryStore.recordEffort(first.effortRecords, activity: first.activityRecords, at: database) + let activity = try UsageHistoryStore.activityRows(at: database) + let effort = try UsageHistoryStore.effortRows(at: database) + let cached = try UsageHistoryStore.cachedTokenRows(at: database) + let second = try scan(home, state: state) + XCTAssertEqual(second.bytesRead, 0) + try UsageHistoryStore.recordEffort(second.effortRecords, activity: second.activityRecords, at: database) + let full = try scan(home) + try UsageHistoryStore.recordEffort(full.effortRecords, activity: full.activityRecords, at: database) + XCTAssertEqual(activity.count, 4) + XCTAssertEqual(try UsageHistoryStore.activityRows(at: database), activity) + XCTAssertEqual(try UsageHistoryStore.effortRows(at: database), effort) + XCTAssertEqual(try UsageHistoryStore.cachedTokenRows(at: database), cached) + XCTAssertEqual( + cached.map { [$0.cachedTokens, $0.tokens] }, [[30, 90], [60, 100]]) + } + } + + func testCachedTokensKeepMaximumAndIgnoreUnknownRows() throws { + try withHome { home in + let database = home.appending(path: "history.sqlite") + try UsageHistoryStore.recordEffort([effort("a", tokens: 50, cached: 20)], at: database) + try UsageHistoryStore.recordEffort([effort("a", tokens: 40, cached: 10)], at: database) + try UsageHistoryStore.recordEffort([effort("a", tokens: 40, cached: nil)], at: database) + try UsageHistoryStore.recordEffort([effort("b", tokens: 70, cached: nil)], at: database) + XCTAssertEqual( + try UsageHistoryStore.cachedTokenRows(at: database), + [HistoryCachedRow(day: "2026-09-12", provider: .codex, cachedTokens: 20, tokens: 50)]) + try UsageHistoryStore.recordEffort([effort("b", tokens: 70, cached: 35)], at: database) + XCTAssertEqual(try UsageHistoryStore.cachedTokenRows(at: database).first?.cachedTokens, 55) + } + } + + func testActivityAggregatesAndRejectsInvalidRecords() throws { + try withHome { home in + let database = home.appending(path: "history.sqlite") + try UsageHistoryStore.recordEffort( + [], + activity: [ + activity("a", kind: .turn, key: "", value: 1000), + activity("b", kind: .turn, key: "", value: 4000), + activity("b", kind: .turn, key: "", value: 3000), + activity("c", kind: .skill, key: "memory-closeout", value: 1), + activity("d", kind: .skill, key: "/private/path", value: 1), + activity("e", kind: .turn, key: "text", value: 1), + activity("f", kind: .turn, key: "", value: -1), + ], at: database) + XCTAssertEqual( + try UsageHistoryStore.activityRows(since: "2026-09-12", through: "2026-09-12", at: database), + [ + HistoryActivityRow( + day: "2026-09-12", provider: .claude, kind: .skill, key: "memory-closeout", count: 1, total: 1, + maximum: 1), + HistoryActivityRow( + day: "2026-09-12", provider: .claude, kind: .turn, key: "", count: 2, total: 5000, + maximum: 4000), + ]) + XCTAssertTrue(try UsageHistoryStore.activityRows(since: "2026-09-13", at: database).isEmpty) + } + } + + func testOldSchemaDatabaseMigratesWithoutLosingEffortRows() throws { + try withHome { home in + let database = home.appending(path: "history.sqlite") + var handle: OpaquePointer? + XCTAssertEqual(sqlite3_open(database.path, &handle), SQLITE_OK) + let legacy = """ + CREATE TABLE effort_events ( + id TEXT NOT NULL PRIMARY KEY, provider TEXT NOT NULL, occurred_at REAL NOT NULL, + day TEXT NOT NULL, model TEXT NOT NULL, effort TEXT NOT NULL, tokens INTEGER NOT NULL + ) WITHOUT ROWID; + INSERT INTO effort_events VALUES ('\(String(repeating: "a", count: 64))', 'codex', 100, + '2026-09-12', 'gpt-6', 'high', 40); + PRAGMA user_version = 1; + """ + XCTAssertEqual(sqlite3_exec(handle, legacy, nil, nil, nil), SQLITE_OK) + sqlite3_close(handle) + + XCTAssertTrue(try UsageHistoryStore.cachedTokenRows(at: database).isEmpty) + XCTAssertTrue(try UsageHistoryStore.activityRows(at: database).isEmpty) + try UsageHistoryStore.recordEffort( + [effort("a", tokens: 50, cached: 25)], activity: [activity("b", kind: .turn, key: "", value: 10)], + at: database) + XCTAssertEqual(try UsageHistoryStore.effortRows(at: database).map(\.tokens), [50]) + XCTAssertEqual(try UsageHistoryStore.cachedTokenRows(at: database).first?.cachedTokens, 25) + XCTAssertEqual(try UsageHistoryStore.activityRows(at: database).first?.total, 10) + } + } + + private func scan(_ home: URL, state: URL? = nil) throws -> TranscriptScanner.Result { + try TranscriptScanner.scan(homeDirectory: home, stateURL: state, now: now) + } + + private func withHome(_ body: (URL) throws -> Void) throws { + let home = FileManager.default.temporaryDirectory.resolvingSymlinksInPath().appending(path: UUID().uuidString) + try FileManager.default.createDirectory(at: home, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: home) } + try body(home) + } + + private func write(_ home: URL, _ path: String, _ lines: [String]) throws { + let file = home.appending(path: path) + try FileManager.default.createDirectory(at: file.deletingLastPathComponent(), withIntermediateDirectories: true) + try Data((lines.joined(separator: "\n") + "\n").utf8).write(to: file) + } + + private func claudeSkill(_ message: String, tool: String, skill: String) -> String { + """ + {"type":"assistant","timestamp":"2026-09-12T12:00:00.000Z","message":{"id":"\(message)","content":[{"type":"text","text":"synthetic"},{"type":"tool_use","id":"\(tool)","name":"Skill","input":{"skill":"\(skill)","args":"synthetic"}}]}} + """ + } + + private func claudeTurn(_ uuid: String, duration: Int, sidechain: Bool = false) -> String { + """ + {"type":"system","subtype":"turn_duration","timestamp":"2026-09-12T12:00:00.000Z","uuid":"\(uuid)","isSidechain":\(sidechain),"durationMs":\(duration)} + """ + } + + private func claudeUsage(_ id: String, cached: Int) -> String { + """ + {"type":"assistant","timestamp":"2026-09-12T12:00:00.000Z","message":{"id":"\(id)","model":"claude-fable-5-1","usage":{"input_tokens":10,"cache_creation_input_tokens":20,"cache_read_input_tokens":\(cached),"output_tokens":30}}} + """ + } + + private func sessionMeta(_ source: String) -> String { + """ + {"type":"session_meta","timestamp":"2026-09-12T11:59:00Z","payload":{"id":"session","source":\(source)}} + """ + } + + private func codexCall(_ input: String, name: String = "exec") -> String { + """ + {"type":"response_item","timestamp":"2026-09-12T12:00:00Z","payload":{"type":"custom_tool_call","name":"\(name)","input":"\(input)"}} + """ + } + + private func codexTurn(_ turn: String, duration: Int) -> String { + """ + {"type":"event_msg","timestamp":"2026-09-12T12:10:00Z","payload":{"type":"task_complete","turn_id":"\(turn)","duration_ms":\(duration),"last_agent_message":"synthetic"}} + """ + } + + private func codexUsage(_ id: String, total: Int, cached: Int) -> String { + """ + {"type":"token_usage_record","timestamp":"2026-09-12T12:05:00Z","payload":{"turn_id":"turn-a","response_id":"\(id)","usage":{"total_tokens":\(total),"cached_input_tokens":\(cached)}}} + """ + } + + private func effort(_ character: String, tokens: Int, cached: Int?) -> EffortUsageRecord { + EffortUsageRecord( + id: String(repeating: character, count: 64), provider: .codex, recordedAt: Date(timeIntervalSince1970: 100), + day: "2026-09-12", model: "gpt-6", effort: "high", tokens: tokens, cachedTokens: cached) + } + + private func activity(_ character: String, kind: HistoryActivityKind, key: String, value: Int) + -> ActivityUsageRecord + { + ActivityUsageRecord( + id: String(repeating: character, count: 64), provider: .claude, + recordedAt: Date(timeIntervalSince1970: 1_789_214_400), day: "2026-09-12", kind: kind, key: key, + value: value) + } +} diff --git a/Tests/TokenGaugeCoreTests/ObservedHistoryTotalsTests.swift b/Tests/TokenGaugeCoreTests/ObservedHistoryTotalsTests.swift index 5378c7c..bfffc63 100644 --- a/Tests/TokenGaugeCoreTests/ObservedHistoryTotalsTests.swift +++ b/Tests/TokenGaugeCoreTests/ObservedHistoryTotalsTests.swift @@ -27,7 +27,7 @@ final class ObservedHistoryTotalsTests: XCTestCase { ('2026-09-07','codex','model-b',70,0), ('2026-09-08','codex','all',300,0), ('2026-09-08','codex','model-a',20,0); - INSERT INTO effort_events VALUES + INSERT INTO effort_events (id, provider, occurred_at, day, model, effort, tokens) VALUES ('a','codex',1,'2026-09-07','model-a','high',100), ('b','codex',1,'2026-09-07','model-b','high',100), ('c','codex',1,'2026-09-08','model-a','high',250); @@ -44,7 +44,7 @@ final class ObservedHistoryTotalsTests: XCTestCase { func testEffortOnlyAndExplicitZeroDaysRemainPresentWhileMissingDaysStayAbsent() throws { try sql( """ - INSERT INTO effort_events VALUES + INSERT INTO effort_events (id, provider, occurred_at, day, model, effort, tokens) VALUES ('a','claude',1,'2026-09-07','fable','high',40), ('b','claude',1,'2026-09-08','fable','high',0); INSERT INTO daily_tokens VALUES ('2026-09-10','claude','all',0,0); @@ -62,7 +62,7 @@ final class ObservedHistoryTotalsTests: XCTestCase { INSERT INTO daily_tokens VALUES ('2026-08-31','codex','all',10,0), ('2026-09-01','codex','all',20,0), ('2026-09-07','codex','all',30,0); - INSERT INTO effort_events VALUES + INSERT INTO effort_events (id, provider, occurred_at, day, model, effort, tokens) VALUES ('a','codex',1,'2026-09-01','gpt','high',25), ('b','claude',1,'2026-09-01','fable','high',40); """) @@ -83,7 +83,7 @@ final class ObservedHistoryTotalsTests: XCTestCase { try sql( """ INSERT INTO daily_tokens VALUES ('2026-09-07','codex','all',100,0); - INSERT INTO effort_events VALUES ('a','codex',1,'2026-09-07','gpt','high',150); + INSERT INTO effort_events (id, provider, occurred_at, day, model, effort, tokens) VALUES ('a','codex',1,'2026-09-07','gpt','high',150); DROP VIEW monthly_totals; DROP VIEW weekly_totals; DROP VIEW daily_totals; diff --git a/Tests/TokenGaugeCoreTests/QuotaForecastTests.swift b/Tests/TokenGaugeCoreTests/QuotaForecastTests.swift new file mode 100644 index 0000000..ba9ddbc --- /dev/null +++ b/Tests/TokenGaugeCoreTests/QuotaForecastTests.swift @@ -0,0 +1,136 @@ +import XCTest + +@testable import TokenGaugeCore + +final class QuotaForecastTests: XCTestCase { + private let now = Date(timeIntervalSince1970: 1_800_000_000) + private let hour: TimeInterval = 3600 + + private func row( + _ offset: Double, used: Double, reset: Date, account: String? = nil + ) -> HistoryQuotaRow { + HistoryQuotaRow( + sampledAt: now.addingTimeInterval(offset * hour), provider: .claude, windowID: "weekly", + displayName: nil, usedPercentage: used, resetsAt: reset, durationMinutes: 10_080, + accountFingerprint: account) + } + + private func forecast( + used: Double, resetIn hours: Double, rows: [HistoryQuotaRow], account: String? = nil + ) -> QuotaForecast? { + let window = QuotaWindow( + id: "weekly", usedPercentage: used, resetsAt: now.addingTimeInterval(hours * hour), + durationMinutes: 10_080, displayName: nil) + return QuotaForecast.make(provider: .claude, window: window, rows: rows, now: now, accountFingerprint: account) + } + + func testSteadyBurnRunsOutBeforeReset() throws { + let reset = now.addingTimeInterval(72 * hour) + let rows = (1...15).map { row(-96 + Double($0) * 6, used: 0.75 * Double($0) * 6, reset: reset) } + let foreign = row(-3, used: 99, reset: reset, account: "b") + let result = try XCTUnwrap(forecast(used: 72, resetIn: 72, rows: rows + [foreign], account: "a")) + XCTAssertEqual(result.points.count, 17) + XCTAssertEqual(try XCTUnwrap(result.pointsPerHour), 0.75, accuracy: 0.001) + guard case .runsOut(let date) = result.verdict else { return XCTFail("\(result.verdict)") } + XCTAssertEqual(date.timeIntervalSince(now), 28 / 0.75 * hour, accuracy: 60) + } + + func testEarlyResetKeepsConsumptionOnBothSides() throws { + let reset = now.addingTimeInterval(72 * hour) + let rows = (1...15).map { index -> HistoryQuotaRow in + let elapsed = Double(index) * 6 + return row(-96 + elapsed, used: elapsed <= 60 ? elapsed : elapsed - 66, reset: reset) + } + let result = try XCTUnwrap(forecast(used: 30, resetIn: 72, rows: rows)) + XCTAssertEqual(result.boosts.map(\.date), [now.addingTimeInterval(-30 * hour)]) + XCTAssertEqual(try XCTUnwrap(result.pointsPerHour), 66 / 72, accuracy: 0.001) + XCTAssertEqual(try XCTUnwrap(result.recentPointsPerHour), 1, accuracy: 0.001) + guard case .tight(let margin) = result.verdict else { return XCTFail("\(result.verdict)") } + XCTAssertEqual(margin, 4, accuracy: 0.01) + } + + func testFlatRecentDaysLast() throws { + let reset = now.addingTimeInterval(48 * hour) + let rows = (1...19).map { row(-120 + Double($0) * 6, used: min(50, Double($0) * 12.5), reset: reset) } + let result = try XCTUnwrap(forecast(used: 50, resetIn: 48, rows: rows)) + XCTAssertEqual(result.pointsPerHour, 0) + XCTAssertEqual(result.verdict, .lasts(margin: 50)) + XCTAssertNil(result.runsOutAt) + } + + func testUntouchedWindowHasNoUsage() throws { + let result = try XCTUnwrap(forecast(used: 0, resetIn: 144, rows: [])) + XCTAssertEqual(result.verdict, .noUsage) + } + + func testPaceSpansThePreviousWindow() throws { + let start = now.addingTimeInterval(-12 * hour) + let previous = (0...9).map { row(-72 + Double($0) * 6, used: 20 + Double($0) * 6, reset: start) } + let current = row(-6, used: 6, reset: now.addingTimeInterval(156 * hour)) + let result = try XCTUnwrap(forecast(used: 12, resetIn: 156, rows: previous + [current])) + XCTAssertEqual(result.points.count, 3) + XCTAssertEqual(try XCTUnwrap(result.pointsPerHour), 66 / 72, accuracy: 0.001) + } + + func testShortWindowHasNoPace() throws { + let result = try XCTUnwrap(forecast(used: 2, resetIn: 166, rows: [])) + XCTAssertNil(result.pointsPerHour) + XCTAssertNil(result.recentPointsPerHour) + XCTAssertEqual(result.verdict, .lasts(margin: 98)) + } + + private func sessionRow(_ offset: Double, used: Double, reset: Date) -> HistoryQuotaRow { + HistoryQuotaRow( + sampledAt: now.addingTimeInterval(offset * hour), provider: .claude, windowID: "five_hour", + displayName: nil, usedPercentage: used, resetsAt: reset, durationMinutes: 300, accountFingerprint: nil) + } + + private func session(used: Double, resetIn hours: Double, rows: [HistoryQuotaRow] = []) -> QuotaForecast? { + let window = QuotaWindow( + id: "five_hour", usedPercentage: used, resetsAt: now.addingTimeInterval(hours * hour), + durationMinutes: 300, displayName: nil) + return QuotaForecast.make(provider: .claude, window: window, rows: rows, now: now) + } + + func testSessionRunsOutBeforeItsReset() throws { + let result = try XCTUnwrap(session(used: 78, resetIn: 1.15)) + XCTAssertTrue(result.isSession) + XCTAssertEqual(try XCTUnwrap(result.pointsPerHour), 78 / 3.85, accuracy: 0.001) + guard case .runsOut(let date) = result.verdict else { return XCTFail("\(result.verdict)") } + XCTAssertEqual(date.timeIntervalSince(now), 22 / (78 / 3.85) * hour, accuracy: 1) + XCTAssertEqual(result.budget, 22 / 1.15, accuracy: 0.001) + } + + func testSessionIgnoresThePreviousSession() throws { + let previous = (0...8).map { + sessionRow(-3 + Double($0) * 0.25, used: Double($0) * 10, reset: now.addingTimeInterval(-hour / 3)) + } + let result = try XCTUnwrap(session(used: 10, resetIn: 5 - 1.0 / 3, rows: previous)) + XCTAssertEqual(try XCTUnwrap(result.pointsPerHour), 30, accuracy: 0.001) + XCTAssertNil(result.recentPointsPerHour) + XCTAssertEqual(result.points.count, 2) + } + + func testSessionRecentHourShowsASlowdown() throws { + let reset = now.addingTimeInterval(2 * hour) + let rows = (1...12).map { sessionRow(-3 + Double($0) * 0.25, used: min(45, Double($0) * 7.5), reset: reset) } + let result = try XCTUnwrap(session(used: 45, resetIn: 2, rows: rows)) + XCTAssertEqual(try XCTUnwrap(result.pointsPerHour), 15, accuracy: 0.001) + XCTAssertEqual(try XCTUnwrap(result.recentPointsPerHour), 0, accuracy: 0.001) + XCTAssertEqual(result.verdict, .lasts(margin: 25)) + } + + func testOtherAccountTimeIsNeitherResetNorPace() throws { + let reset = now.addingTimeInterval(120 * hour) + let before = (30...47).map { row(-Double($0), used: 2 * (48 - Double($0)), reset: reset, account: "a") } + let other = (10...29).map { row(-Double($0) - 0.5, used: 80, reset: now, account: "b") } + let after = (1...9).map { row(-Double($0), used: 10 - Double($0), reset: reset, account: "a") } + let result = try XCTUnwrap(forecast(used: 10, resetIn: 120, rows: before + other + after, account: "a")) + XCTAssertEqual( + result.away, + [DateInterval(start: now.addingTimeInterval(-29.5 * hour), end: now.addingTimeInterval(-9 * hour))]) + XCTAssertEqual(result.boosts, []) + XCTAssertEqual(try XCTUnwrap(result.pointsPerHour), 45 / 27, accuracy: 0.001) + XCTAssertEqual(try XCTUnwrap(result.recentPointsPerHour), 1, accuracy: 0.001) + } +} diff --git a/Tests/TokenGaugeCoreTests/TranscriptScannerTests.swift b/Tests/TokenGaugeCoreTests/TranscriptScannerTests.swift index f2fa15c..ae83ce0 100644 --- a/Tests/TokenGaugeCoreTests/TranscriptScannerTests.swift +++ b/Tests/TokenGaugeCoreTests/TranscriptScannerTests.swift @@ -203,7 +203,7 @@ final class TranscriptScannerTests: XCTestCase { var stored = try XCTUnwrap( JSONSerialization.jsonObject(with: try Data(contentsOf: state)) as? [String: Any]) - XCTAssertEqual(stored["version"] as? Int, 2) + XCTAssertEqual(stored["version"] as? Int, 3) stored["version"] = 1 try JSONSerialization.data(withJSONObject: stored).write(to: state) XCTAssertEqual(try scan(home, state: state).bytesRead, total) @@ -431,6 +431,7 @@ private enum ReferenceScanners { let model: String? let effort: String? let tokens: Int? + let cached: Int? if provider == .codex { guard type == "token_usage_record", let payload = object["payload"] as? [String: Any] else { continue @@ -441,19 +442,22 @@ private enum ReferenceScanners { model = matching?["model"] as? String effort = matching?["effort"] as? String tokens = (payload["usage"] as? [String: Any])?["total_tokens"] as? Int + cached = (payload["usage"] as? [String: Any])?["cached_input_tokens"] as? Int } else { guard type == "assistant", let message = object["message"] as? [String: Any] else { continue } identifier = message["id"] as? String model = message["model"] as? String effort = object["perTurnEffort"] as? String ?? object["effort"] as? String tokens = (message["usage"] as? [String: Any]).map(total) + cached = (message["usage"] as? [String: Any])?["cache_read_input_tokens"] as? Int } guard let identifier, !identifier.isEmpty, let tokens, tokens > 0 else { continue } let id = SHA256.hash(data: Data("\(provider.rawValue):\(identifier)".utf8)) .map { String(format: "%02x", $0) }.joined() let record = EffortUsageRecord( id: id, provider: provider, recordedAt: date, day: day(date, calendar), - model: normalizedModel(model), effort: normalizedEffort(effort), tokens: tokens) + model: normalizedModel(model), effort: normalizedEffort(effort), tokens: tokens, + cachedTokens: cached.map { max(0, $0) }) records[id] = merge(records[id], record) } } @@ -468,7 +472,8 @@ private enum ReferenceScanners { id: existing.id, provider: existing.provider, recordedAt: earliest.recordedAt, day: earliest.day, model: existing.model == "unknown" ? incoming.model : existing.model, effort: existing.effort == "unknown" ? incoming.effort : existing.effort, - tokens: max(existing.tokens, incoming.tokens)) + tokens: max(existing.tokens, incoming.tokens), + cachedTokens: [existing.cachedTokens, incoming.cachedTokens].compactMap { $0 }.max()) } private static func normalizedModel(_ raw: String?) -> String { diff --git a/docs/images/stats.png b/docs/images/stats.png new file mode 100644 index 0000000000000000000000000000000000000000..ae5c3be2a51fdfdfc7e21d95be8d0e2cf27c49a2 GIT binary patch literal 274472 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

!e4SSgv%EraP{~~O^rX6S#g8k{VQk2OoM+) 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 ?49+|I-=+!T1#8|8q; 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 z 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
Vi z3xWnq;|i*UmNbpJ+iD6CfAsu0Ff)1u+}Ug#uMVk|7BxG$@Md(*pl(5~v>qoB?bj8J zd6WP$fi^~%aKNoF7>J!27f#C2rx68wRypDkKiPC*z5iF3NqGaoRu7<&59pCna7f9E z_v{dQF^6=6E{M*=40(W1ijMyWl{G3U8>0dWLo$CE+QWTC9JjOCcRQKtO#N_ZK3tt% zFGZm#T;K$X9&)HK$Rg z4jzX&`DrC(e_&ZwzjYX0Db)=oLIW}B*8cUI-;3_mSKTGvIf9-y;1!lD<&yBKKYq6V zSM)zx4j4hUxRWc2!r15sr=r-0EIT3J8eNf^pKdXlMEK7*Qb(M>KA2%qou8qvWSwW8 z&}#b~?b&y_F(7wdBPoVgv_yxj^6kaYSY3WXLRP<6nMu#KO41QqbkwhdFwz$TE2x^P zzHKX@IyckfrQR5h=0|DLJ0(DABPgfVUX^xU_Vj+ISJYkkTqD=I_17UFZIrgIkcx

envwru(IOhpwII47nhh&N+PMPc!tK zy^w)EsqpEnFFsrAI4MybSmuEkiN=SmMAJPf&4QqeQvf9wn(Wr>>}<4K6~X8HWu>E9 zt{a6>c)5pv_uFvRLb*pq<;GHoy?bM=CvNuDtmy75PlM5^^!kp&LivTrYEuZ=J$2^7!!z0|ocMT_jmh$5^F{(~o-n-vbJI(E zP7`&CxMC@|=R{KvmGFtM?_2@$iB2uZa`=5b6}9mFD(ZS{54W;L@_v7;ny!I$A7-n2 zx~EfWi66^9Ux4RS-yl>wDKR1|Hd+Ga$HJY3;H2O_d(gin{|X%u!5}Y2M_c}j$V^%u zOrc}|p0KbZ88b__V$swtcT6#xZAx zpv)q2r%)YKKF%>4=FnhM&ov;87C@aT|8c>-DMa+P1SbitDO+5}n6ruD71Ur3m?dkX zav%Ayq7uAs@bhn%Z!iNIZr8>H#v%(v2Lg|q1d9&yU*Ibj8UqiefX4 ztPCfg+gU*klDanBbB6aJc=cevF*`f2uKEsN?qsPM59%w1eg~(o>(L6E6VXgpP^9`h z>~BJs{z1g+lVL&8eWZ{1UT-Cqmb@>W)zbysBd8QsCk&zzTWn{ZdZx+<)kD5@K1pvt z$r}80($rzm*>r_q0sRGWJj$-vLyFY+>IYS%(TB)rwO`RsHF^XK)jpaR=!I)y7pPc= zURHa9#ny}s8Po0A?g53|YVwE2dvAKDP8l1Ucg@N&ft8_3$mI{~q>-_F=H@qN_)%$I zb9JCj5ORA*}W89B$%J5&~^}KqNC#Aasoa*TO>U2`pVBx^>%cF4iXRD~X!{d25jbj3f z(sSuL1oPPVc+(J_I6xn2k#5#LGw2RBV7mWkuCMj_4g4vAl2X{1F}I#O;+a$BWx&4z zlFtg&infT<71?eDs6Kw>mHRxE7G7k#hDAM=pgjMAMy4c@ zb<Q+{E5|P+8lKnr)A^dnpK5_b<0B$k*8y4gfZ;8om%`cFX6myQ@Y8^Mmvk?yZ1v*aWa}r;xAts;Q>#U4Y0@+kD*n0z0sfo2<3)sn9wgy|if; z*A+%mQJZ$SwHWr4tIpBKh_bIOu^?RH{6@*y98}v;uW`A0&AR=GOAlDbG~!ny^WAY~ zpU?hCTPV$+!`az4J|sQ;S1Ia{CWF{fn=Dj6;-U)z40!e#`Q_e@s4u}DX<_Hv8a2#3 z>SFSH;=Z@(Ena>EzvT-t$q$P&?i0ArZz=`olw*sziz5W%>f~iUbQFuB%TFy*n9SFL zt+1|p=^QJU^Dk~aO1pG?4SW#4s1<5OPeEaBD#J4r9;5L%^_ zti@!D`o~_I@Qsj&b5Nn>!x)lF;NpX5(D2c=VtV?XXi&57DU_g!tVxSgQPFK##D&jq z=I#tnikP}>`28FK3%|O9fg(7h`X=n=lAF1}&+rZkn{#unq(t?~uxW zHwo*}*p7U4`d5&-lW(sOy>pJya1A)w_c~;dh>kmqwH_D5 zg}bPEkMRkWo;rY7z)eQN%>{IB$1q!8+dy%gTK)LXuM0SGrLO~0mT^@&g zm_=$x>v3j&AY}U=$1F$UldWp9akv_4J+X^a7z+fbD+x7|6CK8m?#X}k^a3&;%~e0m z*VH9{WtCr`2?Osel>eyc-;<^oh^;sZLHa*U>Yo`Kv?MU&emZhB`=fV)Zb~qFl4dpd zf3>$hfw+6o1X2Go^1e`r0P-Kw5bxm{XGdIg86ci>3o0-+8{NlISdgW=aVx@mxK zT>v0%BSa?f19vzHSZLdV{TBTd3s^H;r3)wAWRj$)%DZtz#OEEOV-TX#p=Xj~>MU*a-0`oU& zdN3FR1$vrtDclCCr6z7S2K1vcc4(m6oR}q}%@JrcU@1(|-+yil@Vu2ks=Nlin)nv? z%UH5yuxgxWasQ}8+BF4X0Ri|`KZrmBln*i0UL63PJVPNM1JQ>V0J|~)Mni}Uk)Jj; z;8oIP4@oNA29k$N7pqwcO+zq#;Pr~pM_Wz?oJ7AHwrxDMWfVH&LA*>5q8eWB=EI(V zYpd0Cv^57pzKW2Tpv_ZNBpin7c}j<2j&%^_ect#BRCLMK+APumc{vK~aU_9=2QlaH zfK|(M9tazQ85IQg2eC1NtsC=Te7VA{PR4aoh-tcjav;bls^!bLN`rQ<{8JF@KKV}R z9X~dvz~gcCZK=(i&2Hrh&tY>GdGh1Kr9k|BA}`1oMkBoFd-GlIh9iE;Uy>%BdQ=v@ z{i#HZ8^LEpq*%A{)rnj$kWbf8qXwICke#-`&IVp3cm8G2dD%s`Tw#V%JY1yfe7Gcw zFk_?Xu?Tct@EqY-@^$aBjmk`M<7w3Nbo=ys(W7rx^osdr`8%_a!EUV=6G| zKq@iX*uwGb0L6s1HLqcwf~b=I(XT4`UFM42KHB^o1VOOgei4$uy!qO=OwkEDsBRLy0s1L?n6A6-U5#|OgivraU8phA8w=3Cj?tR zS_kR(xPgf8CZw(>;Bk4$wO!)f*5f0Rlw|HD4u%`9g77=6`fPPGsN7(;g8YX&SJQRo zo!!NEXJ@9&AK~brXR3@ufl%h+2;U;TllnzOa#bkZ)RVd@dH6lo4=ZO=q2*ai*84=$ zCAQ@2q04K*?%< zEF=HXg>8L(eY2k?ACW(n<=103ruVBJbHJjs`cbLk`mAEfZCziNaDt`dw!j+HCqkE- z1K^FOzwQxey~$CFw0){bp1IL?lW<`BG9V_SE!{sQ33ZzfBJ+6+>TZ+Ep ziGZ%l{yc86$^OoO&~Vbt=QWJ{!_m|Nz_P6VsqT&S!B1Syaq#jHV^&?4Wmds{7gVnS<5w+u1fVEP~a)iNAc}RdOZa%Ystep~dh0aM|lrNC`L|O2uQRl-90eqMrN?fOVIP{ct$8jIz`}l1j`7dJ5GJY2$YBlwEjh>M#cl%%UVF@4$Fy zgEMEeBKB0z#X#u(%mI6At2xin;i!Hj?Y`E+$De6zmktwS4R+L!e_cV|8mKEO1|A7P zf<8fbJm1luo6=N;3-=`Z;fT2;zYfc(1!#}y9OUDtq8^BEzIa*s22D45x`9#X^_I-$ zX4i#_gO9M-X>~2oCMlIt41WwI+7@2L+?*44O4$ajSDUT6@cEfKPZv%55-R)zu}qlb zm?OF@K{Rc&2>^buC!7pPKDxgYQh1fvh#ZCvw@)W0?O@ftO(uu)>>Z*{NTUuI1zO;H zC4hOE(qzv)7`??dAZ|{B2{4L!z1cVgmBOp=Z!p$imZM@{a5O&JhX}l75bOI1-vlxUy|zT5tD!ylGmo}w68LfGPia0 z`Odh`Zj`6E5SmNqY`NZ{LvRKYeL`O?dI1{8d`pY%a=CAwUU7#}$ZIWt*qBa6*s>R` zw`l`#xt2nEN^bucm5i*(*lD}dcETMx1@tNm;sciiVPo0!2l;&)xyfKXA7N|I4~k5p z?J-gdlngBN0#&s(rfT*+@da8KK7R^cq1PL=tZP~;i~g%YrJ`hK_S$`DXY*(u3{aZJ z249Ic%#RI~^Svd%I0NDGJmDa+w(Yd0nD4Tz-7P8^nq5yFHHiLQc+aIjS!4fMZO?Cu zyc|CM>ww6;2!TQU$nPs(O%eHBrOA4)a?KT(y9eT)DK$V`#U6lsbG}6tQBf`=8>$rZ znOlRq-_UT6mzaWsV|qt=a%l?B?6*WQ#j~Br03_UE9EJQ;5szNZ2mRuoODa~4P2!sD zhN(>>QZrNM{J6HlMw z#c$1V9|D)I2DVa-g|FkUxsIb8UNRc<9iA3~m| z_4@08;->cRm~d)BioEp%h6*bkLxY2A0v#0sNrU6YVUDO}?-@n=60I3Y@3HYQp5?doE&b!#J%@y zX}4M*glbNfALACJ(VE%w!H{!4%t$3b{ozv^XY=ddtj#m<4((c5?|t&T{jD-4asBDL zE4RX&*fHQGpdSTXyOkp&iR68B@@tj_m1@DlgQ6rGyUlP|Fe9uP#KB_~>iQgx?T zn$P6&!~Xf!;S)*k7#BkVWIc#%8+bui{vFaG%Cf+E!hWN2ro1K>50$T?A?vM)y(#RM z5drGsaQ(^RroTjdnEOUx;9L``(!Urz5EPLv+iV zyVKFagm{n`3a%-9ePDK71zN?O*j6RZyD=jPnQj`y70v`jOPrOL;UZX~n7g({)5`_v z^ncV0Md$Kx8tUVKs$MYdVDTD@;9^sTAx>$o0P_pHwR!ZukgEi+A;ROCk1NuV2$Our zz~YBTU^!u8H`9V!K|)-K7LDYm&~iRwZ=v}(?yEZLAOpQB@&EB<{27)DzxO4I}?-~El4GI(=hMd;hzSS zlp1=G4Km+*lbwZWkX2J5(j<7LjzU`$p(QA%uY+4OS&LBsR)Vj(<+H{c@|*TpIE*C? z+-N*~j;6_pvn>`$vM@jI=o|ohfr7xrHzKZfP)A^~#SmbM-f~d$ddQ-pFy4UQa>goK zgI6loT4bAYUr-(O_E%Md7jFK`I&c$}(83gPdQd&RR%v7;Mlw0QqAL+b?RL5T%-2ARy?hNKT@v~2NmXby$ z;r;AfM=VgNuTGjeL8d0H`C?vZ(MD(9=<#;>QMr>KfT9%Vs$pIfRX`uc^is`;7z&*ySG8~^;iY6Pro zN(RREx&PBue4^mn`+Z36zvH`s;qbJg+JsN|i{D>bEzON3Vz+=f#T| z-0t889FeUu_-kr03ucf4<`frRCMM$BxG!qe^{^YN*^73tYcjeH0*Fc5l@}o&N1ShL~M_SjfP! zXSj*^l7A$g%s1c<#gyUf|NV!A!7*eeR{5`gywf6|fwc5ehjAwM&#}%=;255^aoYcN zEj&T+#{eplLZfEMUlD(QC~%CW{yV?e9}Nng3dkUtjy39k*$m;If@21DUtL4~=vQcO zfzl$nYf(}5*Rf~dSl0^0Db^o_jVuQA00vG>8|wUZ3>k8)Lu`xouZ#5((z%yg+|&5i zvH#C6AR7L&ZS_2*l6TpGZ^XqV-@lK8;PDC>e*eF65UfCILJcbdL9NH@QFn{b>EhY% z>yeMsT{GHZ|GV^PB`|bqWeiSxQ(yO{i~GS+X5;D@6L<)*OQun7-9QGf9u@?YhiYcb z9mr|!#4~{J;lnUTi;61%WPAkz%vGrs&cF6feR*gg_QZfNX);ZGw1X~4N~$>M$^(Cx zI+)V)7HU)l>b`)5#WexJR!Ba+!R0usAqBGo*ycI7%S7j9*P^6(2fu%%#0ml{Xr+w6 zd*atmAeVb3Q8<=PCCB^mL9tMA)mf$J4erPtCpIA=J@Bc-`un@&I{|qpYLKZ(c_dAM z-i#_278m@~D~%*2C=gB4MV747h4u1n3AxT5jilez%g>=j3}1U$_n-dU?-bD&mJs>x%{RV^`TW2s7?{x+Pbut_*@C z1Y%P{iC)`l)e?2`!ouW`n8|F3k1F{a8!rk~isLHo-vMQ?ZYcS(6l|$$zF!2#0<=KP z4LaZubh`{n#{+v);Z`uIx3;dayWYN1&J!-A5}9Q;%Q)?ii!~Ds4yMyGAC6YYUkeBV z(tgiEV^bMYtu;vTtu=zi!5`lY{1^adccE1-b)MCrvr7@QMmSt%ioy-Xs$H-)7r2}B zhRqIdHpHUZ*M(qgvEMEjA5GUB3sf)H$c*&|Kk5x{Q^gb9SC5CafUjjgZm zOvV;^j|a1CUAI`2cTT#%jdydtPY+}WY91aQHScThg8teyvHMDQ0e7fZZ>7z@p`7NjQJQ%x2PY=_zX=rPw92;3=z+7yn zd#}D&@+ggDs>-Q|1%`^@T=DR|EhaE3#z4NRXa+q8TOWc)w$mSlXvRO2>VVaqF-Qyc zax}it^*E-g@dP+~5EYjm@_gl^PL6!lmo+?ldK#N9_`L508Y0XJ4+@vP=7N(s9x3fQ z-&CC1EHNsQe9_jEcd#8QYO5Q} ztnfsVQ&1?XmT4D;ghWQh+ppU=yFFXKSx*e(*5>vCORTn;uE4u$>(dO86cB#HBN-6b zJAi&gZMUw~vkMH&v9nlA-gx|;Ks-`j6AO=sh@(}aPF_2kDx9peTQywI`HM7dp0Voor&H?iI5A*%HG{;XPVPjBz{Xn8rd9&_gl*L8_1Mdx2H zJ(~O^dy?sqkMjT#*-jHR0tx4iEEJdd-Oren88q?9V+9q5pchl_aYi4g( z?Fb?s3*Dm*KUh(7%N#r)0A0Cdp|_9%OrIIR z9VtF6bhYwyKsuPK$g-?K6PVUba;gh#*b%Ih_Gf`)eAv7$sRN6;3M`h4@Mm&za*8Bf z4%Xk^aqX`bPf>%fqG zA2JCgI1wB`khYcKTVk!MtlR{eo8EI0{(|RDld@a`2X&9HW%b+-$Xr2tu!wfQpY($OGW_;IkQ$L{r zq*zu^+Sx@%g8dw+nzwTdiF5>6{@Gz}E$E;SDD1_@#}~(JEJ(0+%nJlaRvi`H`T8!~M!6?TZVXQHbHErBM2d<{_twpPPc8!lQ013Tbk%E$=k~|i zBG3CWMEcZfJWK3th9N;=w0^J}5Lyu|2)tK{B=os@k0L0%umeprGtzjS-VPgGZ}fXB z9WxMe*~9>mPXR@}Y$#dP1?17TEF{F_xI4DK% zKD|emviocO{zlHn_V7&WUvuMubA+X)slCBS<8dh|C2z>PU;1WBv%**|+xNNDXn@)rHg6OmE+BDbx>6PtOV)#1z(4GeGOfs5vRTEu#YYFN1lckX^9v zF)}9TJ_Mie7~viftNn1{bMSAjWxo3+5dO~+l&l^hgF}(aeS2$*&IeHfb!bn(|LMlv zwbiDAPnC#Pjm7j?4ncPGnQbJ~>wRVFCH9z7EjLd19yL}B75B2A|yJr zHRpcSdT3~f-G2k6=vvPDiXxD!!4t3xg86I|M;%6Rp6}tPP zjbiIQkEXP4!NY?)B(^m3^XI{kZ#aV9mB!^dJAyy7j~_nIL{XmYW01&d^MGQXAO6T= z`hcS9%LDgg!|}(iOl5Dqy^yq9&0vbK-vEsBvXXC26m%WPbv0cTLrk@Sm>W3_vMp{S zS);t(C&vAyuJ2z{$z<4=)lBP-78G=D8zMB}G!c}>o8bpOMPZCJ`#eSb*}eqosTVUm z)YNKV4-S?Hy%oKJPB~Pw_t8qnWDpi6sQ*mgr`~FAEgXVn!n_(lS0wI8bK5h|U%H%; zwdcpu!26!O72{PQJcr9ke<58yiFZJPGSoh+C|R$B@mPn;+7DJ&o?UUunHdYj(ppL=-_9RAi%VZhGs1w!hTe}b zR>)}7E|I0F(uwo$v$B64aKwGPHmc|@E}zDLRp1i0hVP$}&*G0{CJMJwsp;jFMxH^u zhSm30+JT6Mho`VajGj0HcDHWXi{^-I9Z%9aKwU-M@7X!6!k|neG@@`m&DllQ@MkEV z(J!~kguZ3tY-qUmfarfZM=yUl)7SbPDa27xb1JKU1=wjgZ6@GjPaUpL(udTQtsa3d zeo#CcR|uIa+NMzr$?MinP7_Qj6%7fFInXjCBYpJkEF0+-Qdq!E?>RsrQsHg?z7)xe z`^rD%w_PB!^Nx#_V@Bt>ohdJIkgh4?B;iu)$zljtd@)vP)M|sjp4n<=0NpxreTE*K$l!{r|Ds(NO6B`u%~%~c z9tDFH`0aNZDCCxnGSyxbeZ0JEG*le)mg96N)I>_(bi}U~id2fjA{On%NAKaQth|G* zs=kpWz!Rd-aEy~{JNDC-v>b+J$$w0VnYD6`|{%z(j!?3 zIrNTfVX5u1rScsbZ{=!wVEOhoy^yCi(BqZiR|cgXbsfGmD1;+Dvk!T5zk0+$yMO=r zF}>`XvvmJ}JNz3LKi>lWheSS@y&j66V9B03p1cc6v4Iael;Ri!6ZMBf=qy-;Qj}T~{2P%lJnGU^v$ zE6ceGt$%CgG)~^&xDkT`qkOsOJsRf|KT*WVBdqKyJpP`Sr1!jNtidyHn>2E}yN%b` z2bYgDSQ6y2l}RMP=Iis`+G^I)hr!ak&}BCU$2bBZujUYPYeSc-YGNTT5m;sK(_wz9 zm~TWa_rOLcu-V@o&gFWd+|odBxic;b2KXgzGrMx?+?+{w9Y0h)we@_)s?MHOpZ)!i8Rf3G!1OJ~-S4K+ zcY+s}n|E)!Q2R0<9?vHTbJ0(DG08)pcT=WFn?(hw%`6A5lT9J_2}gQZT4oAG1bCG0 zdc-dCZ`kz*WQPA_6(S`@(YE4|k;&Y|8af}at29ST~G+Oexv^**?x1`@9 zEnwCEu#a|+j_Z%V!RqVP8+6i9NUPz>%|AKL?nOo2E;e#6{khNDvKoHxv1uxRlmCB` z2hpgaU54=H;y-%Pe}V}b77X;kYm{<%1(+VH_f&sdpZFme`8PrPe;YVleU*`rlXkC8ZBY%ol<-hwh%9feNEO zDgl9(Fscui`V{(nKD-uFvWj3p1-1aHptx@GQ>D#xQLMIutgX|KSZLgLFolM8^C0;j zBJ-CV87r$ILO+D`t&5LX2S zz@bx}s1GhPuP`namZYw38o@6+-PSZwFg=vDN5qkie}L$*6HS*YiY;zVg;ooXV3_g& zqkzM5CPvGa*p$1RNLLATB^=NPezJz4OS))s+NZm*ehK1CX|7wOA$wu?w%o34kajEx zIvwoOnWDaYAp@3xH~@AZ1Sa!MKn0gm%L3*UU`Bgs{-ZJ%QU@@SCJRw}AeEw^Yc#&t zdVi{r5p0DSz$l*Wl9<==o3^V7^LqV%nhllgP9>d6(F-tFuj=mYeG8y@Ia1NoC#R$I z8xUUJ#)dJ-EjB5?gG|AS>X>kciM?-sVv3)NRsULOP+IahWvsC{+eyv=^J;)yU^Yi+D+Bmr{CSFlj&zd0u~?g_z; zN@|qCE$^K%g2-j!lhWSVy2KDL35lz5)UW0At!e>N@H`U1SaiP`n3F@v^=jU4J3e_A zhSu^<+|nY3+_xBCH-BSKCC9Rb``T(7U#jiP=%C^V%#qBeRd4=hcagx8&}_DQsu6 zlr%@PrpT99B)&waX=L|ijYvT*TRc3+`9QPxeDo_|FcojprCmC+%|2R@yL$nx6qCKe zGBhlVWg7&jO%auYhkx2Mso3WA*0206DOvo3VceVQ-wPb)TM@UN`{pCO;q`Eh`_^Oa zpajS}XVex8_4Y>VjO7*Rs9MwJu2O6>m1vV8E4J1kxS8l_{)lDS8T?qQz;3 z?$X*?You!ktjkeD9hCjibFAcnLPoVSCdDMrqiomJ0MqG6HQcFY*GhdnL5~8N!R4EUG7?h3@v6jz3Sd6`7et6g8*6e5_5$p{e12-9+02I>FyM znwXk4X;?0c!KZ@_Qw?v&!OxEgG;XV%unxRjT%#$qawQs7sshZb!pABZYd=aZ%_njS zPp0-N&#Mq47+ma**Ys;NYb+9P^Kyxmp{F%Ts(m@)a0s4?zIl^F-mX@Zg_gx)voI_- zY6GvzKZqWRRPiv#{8&T6xsh)j{T^|t0T#)Nh5r*URb(K*oZ?MPJTEW-505=YKCm7t zbJ0T=8-U+|gFh1$=m_{ctUXxyNB$#;r?m`P7ugMfa7Ye~O7F!2RGlxxe2B~XM_nBi zxXikSHi!HEIQcGY3)SXT$1}CVvlI(9k%TwE)HzU`7_YW-jmK{GX%BtZhQ{{7XV&>2 z+me9OUNwVu{oK_n-iOcaPg%?-D8&pvogLe?)_VmL`{thmyuTZEANZ=`K*tjd--rq1 z$Du%TBYd4Nlv^FNdxA=Q5#%cwphS^k1>QkLu$ZL@H~2oL(&>V;33xz1GL|!10~$N4 z!&?Ivy8Eh>gwNlD>so!&({x=?ZS_afoNh1JtH~7a1y}rd?@L_J698 zgDF4|wvc1Aajhx15!w=u_o75ILOOk2?O(leIYQaG8%nI&09mYer*_3{N1YA>?jM16 zUi99nIhF!O_-FU$$fV)N9rA+NOeqrGA<&j91#8+xh@e$(_tgX3U~ZR+k>nvjS?W?e zJhYKmn8Kuyi~9BJlmjQ?0Lqly-~j7WxH-UG zDC@i*P>fi@{a5?(#dR~DDj=MuiBLo}zx>W7Hy5qO>FDykYJ_eVeG8A8~A1u)s= zS?Is-z?b+XdO>7iUog2F$a06(M^CzaB_QRt8#`RXf)z~=P3b&(2B50EPQMc*WQzE} z)#0=zS|hp@&6tIlixTvz&Awt3B#7_$^0{xPex#LK^jT%aup?I(-9f{aQRU*aBBAQ= z*O|^$q;mg^p$A2|FtTT^4a~jd;__FTVe&t-V{OyNaeMRvaWoH5mfF1APVy{X7s1lK zspa%P?#K?|g}6@5P;_hqsrRgZu{=+Hqzfkg9O0Z3EsS<@w)@C%aYG&fBRgdFyna36 z*n)3EuImXOp!JSBwkfxQ!n)HuMSA`_bKnbF?PzJ8V+7KS;+qW3#mk=G>LJ(w4B#-2 zM52H8$^Ugm6PxWt2del_poXBl7StT&4M%5Q9~s#9SR@V|{C7Dik}M2rO_hVRJ#m6N zv|HT5(-`YsrKmlyGs?ZAX2o3bd;02?Q|j}tNl6xFUoOP)(z4k&JAaQ|!anV$@7!In zs*Da8E>^QDIozdM;qUV6emQ6f9E9@a-f{jjGX+tch9%gHTD8k<*QjoQg3su78{3= zdEO*)&wPSvQ?^|^gzKH z>baS7)i7hgQohGdn~FlFB~w%+vZwl8%V_gq zT+4ZBU%L}z(-D)_zGajcrQE_oU3x}!Z9008Pn0Nsr$iuq9Po*6JCOM{Xs!19HwVyX zour~?oAc^T{zgbG==ICSZ4*sd`;ihCmsbQ>-8sD`*M29Cs$@CwL&L)=jWC6!XvK(@ zV;QYK4zn46bHvk{UST{e5>Ikb3NAHmESa>Sj|G&BNNoyE<3bBO;d9){V7h5KY|Zax zXjO%J9m;Qnmbrr;OxgjNop`+(P$AnpapXl5h_p8sE2u%1(Mo>$#Rk!7DhsrsdX*)^ zS?K~v1?sCQwySo{oJ8vnEt`fPSI6St!RR0arUI@ciXRPO?o)e`sOe^<__S{p&0|H0 zhZG0K#U!-+lTy8%Z%T~myEhwIj!at zh@YiaejUUjE{u*!ioG6_^eb#js{szVY|>%6*kx>5jv8{l$kZWcXbwusu3rZib4Jlq zvLTLGzyL*;$=po~vxq6rZ&N?8O>q70vatIIG$FLCQh$8IKa?({r!W$XPAfkDtgoj} zHt5YYTFZB=3=~@CY6DVh&s$w17+%xj5p_w^P5CM}Kk{yn7@Jj|M+x4vEGOO>6}?uT z0ev*e7ce=YBHBa1{1!$XpIe0d4aObM&{W%9qX_#BL@rmbp*&i4@29%3(~z*&us+{h zbI_JECF^nd!aEGt?an@hF*iVSpIO=ib6I@SOxmIP?BZ~jAz>tC$FvD(pnt+N+?}K~ z@|uuzNrSW??+2rA1d7TXiSKEuQia8CU>8^6bQO_ZiZv?Ma1%K!Ymc$>UcAN>c!T9O z-865SKCD9iL_H#H`{}bYwd9-}WVWQ8ARg<8YRQ>TR*sw#1~8IEd>=O8v-_Kp`|P0e zp<7ZqgmTlpG!6W5&lKHX(9k5NHZ(J2X>)e{{24>56mEQ{q$~(*{Ec|qhj)=rd2~#N zla!eHOfz4g$-#`Ij7H-ZPr~rL`+2OCU)y%p7%Ss0+coS^#LSXZE6w`mt1F`Cw+n>h zJbt3l#?|5vjUU&Ww zHT7he>vYJ3x#cVb5lJ{Kh@%#hi%1Ze#q(xg&elxw`jcmG7=EeSJb3}W(^1ct618MG z*xc>Xb<=duk$N5)ymYIUp&_|XXwVyqx9g;b3fJPLdU*Z)Jmt=PDCM?ZI%!UP`-HCM z7adwlF|_iCg6AP5L5&Us5uw!H%`eoWU%K-KmXSgh9KR9T9U4wnG&t&)eWj(^bz#P$ zK4&u2dR%|V#ITpMoZ{gcfS=7vO)`ie&!{)C)-$f$u~ zr5iU$<&R9@V+RRMMX052|K+%NEdw-*VCLt=fAuM>LSkJ+<-DVbf3yhS1YlS}(<4&- zcY^TG_O?pf8-9bU`WL5xW@p%vi^70 zWJKUt;JV5-&tDe{4WhlQg?1JFD@H-i`u~dyh=wmmqf@HMQK(4u7C_^|$>~$%TBf!& zLDXsYXI&(bpoh8_NZkUPh+*O2TEw4oUP1BshF&l}K7O(qbSRzsHr5zG1Wt~+9!CiU z4cdnF9!?6XEoW8julDsGndsHZ5Z$R^{*|K8l3Rc~M0wm>XbCd(uynzBl}TMy(GtXr!df zPvM?_0q@Zwqg1|lCXn7iuIsZN&uTyht(Id^MWq=lS)idnXKVO2PDDi%A?sDFCYnx| ze#wBA?Qqmm^M-meO2F!Ob`H21fUOD@lP z5`sQQFr{$f5@3Sfim&9N&ya{zRJ-^!;mI{YwV(j7nZrSBPGt4)9ohjsq4Ds~z|A9k zgx-EyE81c)QRG}Q74|u1CwKs(P%|`S277h=x;OpQ6?aocuJxC68yTNmI&RnJH%L&> zybo=_{o;Dt*g7w#ZVfeX86iJ-c6sDtj zgRP#o#^YH%V*Jr^e2s~Pin$?o=jD;o08bCrp~9d;BYjJCR+mrm=Bz<{*!%97rV=!e zIxH?fK5%^w4}a(64x;{HrzH?^q(p>t42q7EQ`PO_SEg4zDDo50b=x(;2RIN=`XqWJ zf4rOjI9hP2F8X6i6nG7VQy;?{4QDqvL)JtmDBZOi1R#K8>&6{fw2;(LI z=Djz+gVItMx)f&wS@#v)a(kaXc|y4(_2#wm_*d2(S)D>m+)eD~ly|xm;X(*1EXr}z zD%m+YrDoR;vkp;-|4)189Z%)||9?b|m17gKDtkqC_Lfohj%>-6@y-sBG)P$;DSL0S zvLl-i%1E~Cky3Xr$e?Ersq%O^P<}?EJ8=H%n zS(a$;cfj%Ce&KSZQAIw>79p=G>{9tsc;m_aEKqn5xs>@i&3&tlAs`$3_;cRaTj<<+ z7CU%7Pwo2kEjBHX#&m#O=jPa55XUTj&f^62hjx!eXeGV4*Yz-BhIAu}p5)Fa1(2(f zt6gIAus>&*!T~VEHb-uQk(tAyOt7kj=SKdb>ev-Hv@+j z6%Iok$b=#X(c?hcTp$*ZY?VCZ0emc08Z=MIRfL?N0r}*E{n{}IU&epTsUrLSHp5{h z-xlaQF01=q2cO#@hx6i_&dHb9jma1i8Fdi@)80dmqhm3 zky1nqe!e=l=L|I#VscgO9^=rN|er%72j4$9WMP3>6n#K zFo_guR)^Gz5{Gh#!ci31xTZKC>Q)IG_rYxCQPE*Pdbt0MAPh7!v~5v(jfZdnczm&0o231; z|4P7?rik_ZjH8fvp%j&y7~aNIn~X`IyJhap_DVAE5vuZ~81?EHU78s_ zQ&{N6f~FncDYz=m;N19-q4}Ce+iC5Qh6;^MXoYCxD5QyPH2WH}C0&7JwJQ99O)mz` zu@5t1emi5?4@i63(Mp#@Xf@8!67%-$V=^2Au#02i&1d&~Z%>2*Kah%2{1tF*lV#9y z>kgky`2?93Lp2(9i~gHkuQnz_9+;`;HXEyf3+mJT+y!>%V;ho0d-Xogtl)Ivc^fNK z<5N!?<{nsCS+#>aj;3qcY0!5?UA_dXpZ~*U&Ev#K_0dY#G}4g#V5C$Mz>$~iMS~jk zM>0P@ob3u!1sA(rRuoKtE8Hk8MZYhG)&fjmPFQF zKxW#B4t)moX4-6NS@xNlcA&e$O|x47NXX zu8=JXl>2P=%aTzwUzHv$%C%#xqP$^HAii|VD6x#rwC-G3t5ANh-p@HP#Z4hu+?~~l zxl()!4A3*`s()QeYUD9-@$ho{(t8CO?x@@I5*L}UT%W(uXC%a&TD#fU`vC+2k3lH4 zCHW)x+hrcJEpuKoD}1)7{SqGQEZp5oLANfsFpq8i#xq~KzwuYceedoAK4PogoLunlDpUixk3{H^aqnmWQb?w z`NLzWqZq;byu9Do)5(jnsh?cHQr}R<@mzR!E{3%R710*TN|D73+-9A%`uY;J1rE62 zq^u%XK5za=Y|CyDVR$HPm7WqiKUmJAbpNH7&quv^QM*`1Cs`c*_>KE zv9kv4D+~^OuLy04rGftb!MmVQbuf7$#YQ}ZR|q+W8lvrmgnwX;TtK3LZFi=<_+e4B zw8EWBz5`Qdn~mjFJ6u?H7v>hvD<~;-a#uX!bgNxc45rIKCam3hvq{DyLFjx@S?Zm- zu(k;K zmif5dmt1BYUUy-|9&d!jTc3Wjn}cMouje14H{VywmOK%7E?qr~`6lM^GEYLPjx8qJ zV)l$9{0ZB+qsBA7Fn0IZ6I`(StF~!ubLaqV_|dpE(C6NtWfpAi3tF3QZM=h~WP7%h zdW|m(k)Fgl$y0&nVS$JrKG4=&LVYBV8^$N~U6?ycW(`e$C@`G@Yrf56taRJ|SaN-m zK+?@c-AJsTXY_8`ZvAjL?304ml`({l3As19C-H&!YNhGyMY$x_XD!0cW`iM-_g$Wr zv2KC3=Y}w;*aD-zZQJ9=DL<7ntX5(}FMHf?ngB7lOAN4V{fpG!`q;A{EY>;yrHn;-w>Y@K1qe2c;@#i|z8B0>w=ZF`c*A zxnbD*iIDM~cm3YE($<;uHzK-mfx8qTon+M1E1R?o>u>Gs8>^V?$S^v36_># zP-M5Z5$etDt=ah9!IpTRN5?GirF+rpWFCQHBnQG-{8b+IBJ@Y6l6JDPva(|i7ML^i zoqd`meq+I!x+meP(nRJwMYs*d_luroAb^%}nXxZf4LPS>{mQhUjXFgpLcXSt%c!(H zf_R}oFXJtt_5yeASYt^R*`jg0p8%1}B9|6{wzA97l`(0$r5NT0wMb0o zC-ZONwtt5ums#ZjKVt#=6|&0nb*t7yT`s0Qkew7XjhEE<@j3BLVgk1}<+&7Xn#q(g z9h8npA)EoT0hi;!tnFat>x9=C^#d*ylGwe3R*F*`>e&3Tm_ zqTi25I1^HnbiE}TJw$yUS?A9#cp_K_8IXZw1Ovk+?q!SLAB71e~P8?N5 zz1hFN@tu7mTx#3~k*215c*#;>-DbU&*101sMni|0JVnW=vg$Rc;;NugiJIqk{$h@{ zK{%~d=Zm}zKr)dgd(%4~z|Ct8>zlM^M@zSBw)#1&20r?c!KuR?aYC=T6|*lP7gh!%r6*dcJ1gkAGGGPi zOsRwA&6X6g6a@&kk}X=QHO0IZ_~H4KXat|k{8=l)z%vB@K%|h6&g4{g^0T4bZ)Svy z*lyIB=H0ZkES@RLP3l2@%X?txml8L}#FVsl`AoqA)J4`xk|%t1)lRj2?H|Q%Z`)ij z?G4j-#UANhSw|&m$zz4$Ah>yNZjr)pid0bphd{o+mCJMT*hn&D=S&pv^~9W< zD%nRsz9LNd^iHMw|Noypr2Q`0mWMz1gPxhy=09nTrv0OdfKq}Kp@Lq|s}6%tARg=E zr%y0HyrWAMF!arIl)|@ILgb%=k~SbiI0k(IIfBph!}?6x$?Tl`SO`82Le%y1?U44bUI#v z@C1s$`a9u-+rQ#hmaBySbI9IGjGzvAlv!t)ry9r6l0Op}$%F*N3B5Jx*I()Je?M{= z)GDZFKo%lM5yn^~txe_X$s#J*B&@o(nJ;nfzM=c{Db6&*{o-%S5zC^Lhvqf&m7p)i zc@`g^ads>v_q8TrUPxcxIZWThLHp4PXY;2l*zOFm%yC4|aln=esmjUy#nuF(0g`RV z6m1AI`Bw7?I$GnPIxcfR;K;H15W1#1CZt@ae?RYteb{h=far%(m+}5fWMw z$N^7U&lTr2s(j&xBfE#Omf|3%eGCC3Te+GuFix?W4_kDblFbJOu5hh>J$C}4$k9G5 z-{ZDsdD@;$!u?-M&Yy}Cud3F}Oz^km==qqGL;2f!*7ry(}O!6Fqf1qh z^GO$34)gnfKaMWLG6&tEXi=A20ck1m?GUYRmCt$iP35l&`vJvtjbtu1ZCZa6Y}B8i z6MWE_Zm;-fJZn4XD(w0iGnX!I28D0X3TE;GZqWwO&YNY3%O8J12A2zk5=wo=o7L4J zPeonsZFx2g%b3RKl7;m(B@OWuP!90h=uur^Pm#WJC%1}}J7%7490t~MEab{YU;GOm zn1ZqC5)+eI+=GRVIC@?p-5jx$G8cpejTv389v}~>0M>~D=|%zwK+N3Sp82l#9-sxg zqVLTDj)>%t=2}sHTk}jX7E2SpF#{VTnz?@EFQxX}?mud7TmAazVZ_iO@F`?5BPYCq zW8~xWjEh%2pN}2}CvHOkF-62pgFAN~0VEOBPMVIMi#ONncuX%oM;T&BC|~kkLl;@`Sc%+H;;K52F^3oGK zgx1Opcm3yKc;y?{D#aCe1P~1*WHcWgP5ZlUZKJ#v2UQ@liiZZg^;7F~4pu++pFg60 zIxV0iF-GFxx;)}q|28vJ{}7l4m^Q6bG(cs5PN3?VF#^@7skpd8_kj}r`{M5l!IJ1>)vMj#~+L%G1M-42yniFkVUdIhw>(!k{Jb@9Pn%zY8>AX1 z@r__dq_a5rLD@rwqid90dopESvLj5(L}9ibaAO;EbAB9F$GnF6)d}FFAX3(D8hGmU zph$z;1{&Qs1D(e2RAa~eeqK||oc4dm)Y~%)k{1VY@uO5}Pxq@J6mNZ`?r9pG*lw!I z;mzvq=9VM`ziNr1F$QD?OP?P!4qr_t`>ab92weofi$C<7()ekXJ zzQBawd~!;ikeGV1?owWWiJwq<#%qDQ$8y<{DG6kG)-L}<^994R%ScGhRh9Vd@@R)L zZxMB)!0(v32$sY_6aAYt$@R|Bm>ntf4_l zh(`P9xsKE!T&cL^f*8p2Ky@}Z7}Ib6j(=v=2`Pn-9&fmiwDV5gdu84rq5Z+LrK3QIigzQx zL&f6*;({WfON~L-qR(J*a_*9v08s&J!eW_%!ai%;Vrz8P272YJDV3+y&Ejobb`|z( zlO`q4M?Z%|nmH+7dY7lit;Bli+E054r!UVr9)!PliX_WaQBhG4QtI({I@l3i#L`@P z7w@GVMG^%L*~_&L&$C_@x;l67C(r!?pSLv)7$CSR)fdi}SwYpN~L%)FfP^WgB*`P2n^3m(YV&{CLc zlS5wy>R1Pwuj7r1bO(QqPteeHqwHMUTJCmw^!vY0j<4^5V&QA)(klZQF)01t>ocr6 zd$b#&O>6X_XK?XLhGWyuAwGt%k_mx~O!3@S7e|QZ&6}wu*@Cl{46)*OMv!IV5*ZO` zEdm$J8wbOA5M?T$nU;F;EH&WwH{9i4j8o25R-rI}$Y|#W+jjZlQ4Gbr zMNEMS&{c))cDW38$2RsmA%So?JoV`V)h!g%rQ%5|d8E4}OXkOK2rAqi&N z2qvt(*oEoZjQy+uWVMZT9tRyg-^x|bVOvygTjA32nh4q!s5;2>{9M9LnYE1#&Az-o zTgDSJz44K!d{SoH!@a{z)!ZA;m;p>)PSd7r<}03IEG>Xxo`bgI(jC`X!-~Fv%2jyB zcscM&Ug-!FSz}iz zSX8Uf()_VDE7(?4F_=9&*SW5SE{Ya@y@+JPLWY?8@5e3Hg!Y6^SH)cTng5z}TZh3z zYr_ip6s?5*3-Xs{(jjXsk*A4y}dTFS`LwBB9T@W!CfpD z!J8TT^5wEOH|HGLhl)9ib{R^9oSSit#pJZ%fbibt2s%9hO56=8s$5c8RHPo`Q3ZH; zy?EMG3T8T&A=r+~#Z7hXfZm~&bY1R4v`Ip&Zrz2LlV;VCn80qgz>#6;mw6JIdbjLU z3lMT8QLuXGTJPJ^y3nY@K+B6AN zXD+KH>o^Y8!NOlyMuc@2gZ^?5VcqJ^IN62NQ}Lzjeu{-WGPKb=h%} zDnE1DxljHTsB?{kgTlc%$4wRCi+vsS9UU3TL4RrW-#ibr%!3chsKlO#D)S8Pr;tc8i1-f{D`;<5PEW)ir1;@_a#B&` z*iX#x6Nu8Uny^Q*5Ak49WNLjAkx44l`~EKb>b;X*J4{kGeJpg}z0tW{ZaMYuPWYmh7#1?U*MTo@LRJ6f!VZ2WE`@F=e$U5`H>L$IU#|8j`(bwS zWNEBfMU3-A%oGXSyL#V%>KP}($T8h`u&~PycQ+94N z8-X}o)EX*|3A0ee5WO6>9KN7Pvam6OwzSrkmRDJX9QbErP63-KWsGY(sVyGk_6k>5 z9Cs)!<{>Xe!ELKP5dXng0U=^h?{o2p(c%!P}G`Z=Wb=5H)$mOj-{1} ziAe@$wzW+uL9_yE-!KoqwV~;hutpjo{j$+9d%$1TndwIjiQw3JNUb#h_>i zLx4WWs~G2BK$)+u!ION7Q`ReG##lVYZt}*#U%Hy1#tya?g-OP}2pxPU(wA;xKAbzwqVF@k13$v1p~2vXZ~{#i@b_iHz#H zmgo>w)S@aD`PmiUR_Zei)r~_3VD~$DNhe=n7^U+|+gi}&QpRHGHuWRg`5D!e$=G6_kiQx2w^nV9=^6LDrIFmuYi3>9ghJ>junP9*l6$K6XeA!#U{{wFCM%w@Y literal 0 HcmV?d00001 diff --git a/scripts/usage_history.sh b/scripts/usage_history.sh index d8d8b10..9da2488 100755 --- a/scripts/usage_history.sh +++ b/scripts/usage_history.sh @@ -49,6 +49,11 @@ quota) datetime(resets_at, 'unixepoch', 'localtime') AS resets FROM quota_samples ORDER BY sampled_at DESC LIMIT ${1:-40};" ;; +skills) + run "SELECT provider, key AS skill, COUNT(*) AS uses FROM activity_events + WHERE kind = 'skill' AND day >= date('now', 'localtime', '-${1:-30} days') + GROUP BY provider, key ORDER BY uses DESC, provider, key LIMIT 20;" + ;; export) OUT="${1:-usage-history.csv}" sqlite3 -header -csv "$DB" "SELECT day, provider, model, tokens FROM daily_tokens ORDER BY day, provider, model;" >"$OUT" @@ -58,7 +63,7 @@ sql) run "${1:?usage: usage_history.sh sql \"SELECT ...\"}" ;; *) - printf "usage: %s {status|daily|models|weekly|monthly|quota|export|sql|path} [argument]\n" "$(basename "$0")" >&2 + printf "usage: %s {status|daily|models|weekly|monthly|quota|skills|export|sql|path} [argument]\n" "$(basename "$0")" >&2 exit 64 ;; esac From 554ef77a51056d78b472a4f19490a6c5b890ea80 Mon Sep 17 00:00:00 2001 From: Steven Coaila Date: Sat, 26 Sep 2026 10:34:25 -0500 Subject: [PATCH 2/3] chore(release): prepare version 1.7.0 --- CHANGELOG.md | 20 ++++++++++++++++++++ Resources/Info.plist | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bc11d9..4f5ead7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.7.0] - 2026-09-26 + +### Added + +- Stats: flip the panel to see statistics for the selected tab, Claude, Codex or both. The main card answers whether each weekly limit and the current 5-hour session will last until the reset, with a projection chart, your pace and the most you can use per day or per hour. +- Stats also compares today with a usual day, charts the last 14 days by model, and shows this week, this month, the total, best day, streak, active days, top models, reasoning effort, most used skills, time worked today, the longest turn and the share served from cache. +- Under the 5-hour session in the Claude and Codex views, a line says whether the session lasts at your current pace or when it runs out. +- Stats cards can be collapsed from their header, and the choice is remembered. + +### Changed + +- The period switcher and year stepper are redrawn with the rest of the panel, and the streak sits centered between them. +- Model names include their version, such as Opus 5.5 or GPT-5.3 Codex. When several models do not fit, the first one is shown and the rest appear on hover. + +### Fixed + +- A click inside the panel right after reopening it no longer closes it. +- Forecasts ignore the time spent on another Claude account, and returning to an account is no longer shown as an early reset. +- A very large first history scan resumes where it stopped instead of starting over. + ## [1.6.0] - 2026-09-23 ### Added diff --git a/Resources/Info.plist b/Resources/Info.plist index 840ec27..821b810 100644 --- a/Resources/Info.plist +++ b/Resources/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.6.0 + 1.7.0 CFBundleVersion - 9 + 10 LSMinimumSystemVersion 14.0 LSUIElement From 9c5e2bd2238964a62224c6635d74df0763ba247d Mon Sep 17 00:00:00 2001 From: Steven Coaila Date: Sat, 26 Sep 2026 10:37:32 -0500 Subject: [PATCH 3/3] fix(stats): split expressions that time out the CI type checker --- Sources/TokenGaugeApp/Support/StatsSummary.swift | 11 +++++++---- Sources/TokenGaugeApp/Views/StatsBarsView.swift | 8 +++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Sources/TokenGaugeApp/Support/StatsSummary.swift b/Sources/TokenGaugeApp/Support/StatsSummary.swift index 0ea9c0e..9579144 100644 --- a/Sources/TokenGaugeApp/Support/StatsSummary.swift +++ b/Sources/TokenGaugeApp/Support/StatsSummary.swift @@ -247,10 +247,13 @@ struct StatsSummary: Equatable, Sendable { let current = totals[row.label] totals[row.label] = ((current?.0 ?? 0) &+ row.tokens, current?.1 ?? row.provider) } - return totals.map { StatsRankItem(id: $0.key, label: $0.key, value: $0.value.0, provider: $0.value.1) } - .filter { $0.value > 0 } - .sorted { $0.value == $1.value ? $0.label < $1.label : $0.value > $1.value } - .prefix(limit).map { $0 } + let items: [StatsRankItem] = totals.compactMap { key, value in + value.0 > 0 ? StatsRankItem(id: key, label: key, value: value.0, provider: value.1) : nil + } + let sorted = items.sorted { lhs, rhs in + lhs.value == rhs.value ? lhs.label < rhs.label : lhs.value > rhs.value + } + return Array(sorted.prefix(limit)) } private static func insight( diff --git a/Sources/TokenGaugeApp/Views/StatsBarsView.swift b/Sources/TokenGaugeApp/Views/StatsBarsView.swift index 71ddcb8..6f24aca 100644 --- a/Sources/TokenGaugeApp/Views/StatsBarsView.swift +++ b/Sources/TokenGaugeApp/Views/StatsBarsView.swift @@ -84,7 +84,7 @@ struct StatsBarsView: View { ForEach(day.segments) { segment in Rectangle() .fill(Self.color(segment, providers: providers)) - .frame(height: max(0, height * CGFloat(segment.tokens) / CGFloat(max(1, day.total ?? 1)) - 1)) + .frame(height: segmentHeight(segment, of: day, height: height)) } } .frame(height: max(day.total == nil ? 0 : 2, height), alignment: .bottom) @@ -113,6 +113,12 @@ struct StatsBarsView: View { + (day.total.map(UsageFormatters.tokens) ?? "history.unknown".localized)) } + private func segmentHeight(_ segment: StatsSegment, of day: StatsDayTotal, height: CGFloat) -> CGFloat { + let total = CGFloat(max(1, day.total ?? 1)) + let share = CGFloat(segment.tokens) / total + return max(0, height * share - 1) + } + private func typicalLine(_ typical: Int) -> some View { let y = chartHeight - CGFloat(typical) / CGFloat(maximum) * chartHeight return ZStack(alignment: .topLeading) {