@sriinnu/runic 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +1 -1
  3. package/README.md +47 -29
  4. package/RuniciOS/Core/RunicTypographyiOS.swift +24 -0
  5. package/RuniciOS/Views/ProviderDetailView.swift +9 -9
  6. package/RuniciOS/Views/ProviderListView.swift +10 -10
  7. package/RuniciOS/Widgets/LargeWidget.swift +14 -14
  8. package/RuniciOS/Widgets/LiveActivity.swift +19 -19
  9. package/RuniciOS/Widgets/LockScreenWidget.swift +15 -15
  10. package/RuniciOS/Widgets/MediumWidget.swift +10 -10
  11. package/RuniciOS/Widgets/RunicWidget.swift +5 -5
  12. package/RuniciOS/Widgets/SmallWidget.swift +9 -9
  13. package/Scripts/package_app.sh +2 -2
  14. package/Sources/Runic/About.swift +3 -3
  15. package/Sources/Runic/AppNotifications.swift +5 -3
  16. package/Sources/Runic/Controllers/StatusItemController+Actions.swift +17 -4
  17. package/Sources/Runic/Controllers/StatusItemController+Animation.swift +5 -4
  18. package/Sources/Runic/Controllers/StatusItemController+ChartSubmenus.swift +30 -16
  19. package/Sources/Runic/Controllers/StatusItemController+CustomProviders.swift +6 -3
  20. package/Sources/Runic/Controllers/StatusItemController+Menu.swift +47 -14
  21. package/Sources/Runic/Controllers/StatusItemController+MenuCards.swift +6 -4
  22. package/Sources/Runic/Controllers/StatusItemController+MenuHelpers.swift +40 -12
  23. package/Sources/Runic/Controllers/StatusItemController.swift +43 -43
  24. package/Sources/Runic/Core/BudgetNotificationManager.swift +2 -1
  25. package/Sources/Runic/Core/Login/ClaudeLoginRunner.swift +2 -2
  26. package/Sources/Runic/Core/Login/CodexLoginRunner.swift +1 -1
  27. package/Sources/Runic/Core/Login/CursorLoginRunner.swift +4 -4
  28. package/Sources/Runic/Core/Login/GeminiLoginRunner.swift +3 -6
  29. package/Sources/Runic/Core/Rendering/IconRenderer.swift +91 -85
  30. package/Sources/Runic/Core/Rendering/IconView.swift +3 -1
  31. package/Sources/Runic/Core/RunicColors.swift +6 -6
  32. package/Sources/Runic/Core/RunicSpacing.swift +7 -7
  33. package/Sources/Runic/Core/RunicTypography.swift +206 -0
  34. package/Sources/Runic/Core/Stores/AuggieTokenStore.swift +10 -36
  35. package/Sources/Runic/Core/Stores/AzureOpenAITokenStore.swift +10 -36
  36. package/Sources/Runic/Core/Stores/CerebrasTokenStore.swift +10 -36
  37. package/Sources/Runic/Core/Stores/CohereTokenStore.swift +10 -36
  38. package/Sources/Runic/Core/Stores/CopilotTokenStore.swift +10 -36
  39. package/Sources/Runic/Core/Stores/CustomProviderStore.swift +10 -10
  40. package/Sources/Runic/Core/Stores/DeepSeekTokenStore.swift +10 -36
  41. package/Sources/Runic/Core/Stores/FireworksTokenStore.swift +10 -36
  42. package/Sources/Runic/Core/Stores/GroqTokenStore.swift +10 -36
  43. package/Sources/Runic/Core/Stores/KimiTokenStore.swift +10 -36
  44. package/Sources/Runic/Core/Stores/MiniMaxCookieHeaderStore.swift +10 -36
  45. package/Sources/Runic/Core/Stores/MiniMaxGroupIDStore.swift +10 -36
  46. package/Sources/Runic/Core/Stores/MiniMaxTokenStore.swift +10 -36
  47. package/Sources/Runic/Core/Stores/MistralTokenStore.swift +10 -36
  48. package/Sources/Runic/Core/Stores/OpenRouterTokenStore.swift +10 -36
  49. package/Sources/Runic/Core/Stores/PerplexityTokenStore.swift +10 -36
  50. package/Sources/Runic/Core/Stores/ProviderToggleStore.swift +1 -1
  51. package/Sources/Runic/Core/Stores/QwenTokenStore.swift +10 -36
  52. package/Sources/Runic/Core/Stores/SambaNovaTokenStore.swift +10 -36
  53. package/Sources/Runic/Core/Stores/SettingsStore.swift +99 -52
  54. package/Sources/Runic/Core/Stores/TogetherTokenStore.swift +10 -36
  55. package/Sources/Runic/Core/Stores/UsageStore+Accessors.swift +19 -6
  56. package/Sources/Runic/Core/Stores/UsageStore+TokenCost.swift +1 -1
  57. package/Sources/Runic/Core/Stores/UsageStore+WidgetSnapshot.swift +1 -1
  58. package/Sources/Runic/Core/Stores/UsageStore.swift +234 -167
  59. package/Sources/Runic/Core/Stores/XAITokenStore.swift +10 -36
  60. package/Sources/Runic/Core/Stores/ZaiTokenStore.swift +10 -36
  61. package/Sources/Runic/Core/UsageExporter.swift +8 -4
  62. package/Sources/Runic/ProviderRegistry.swift +1 -1
  63. package/Sources/Runic/Providers/Antigravity/AntigravityProviderImplementation.swift +1 -1
  64. package/Sources/Runic/Providers/Auggie/AuggieProviderImplementation.swift +1 -1
  65. package/Sources/Runic/Providers/Azure/AzureProviderImplementation.swift +1 -1
  66. package/Sources/Runic/Providers/Bedrock/BedrockProviderImplementation.swift +1 -1
  67. package/Sources/Runic/Providers/Cerebras/CerebrasProviderImplementation.swift +1 -1
  68. package/Sources/Runic/Providers/Codex/CodexProviderImplementation.swift +1 -1
  69. package/Sources/Runic/Providers/Cohere/CohereProviderImplementation.swift +1 -1
  70. package/Sources/Runic/Providers/Cursor/CursorProviderImplementation.swift +1 -1
  71. package/Sources/Runic/Providers/DeepSeek/DeepSeekProviderImplementation.swift +1 -1
  72. package/Sources/Runic/Providers/Factory/FactoryProviderImplementation.swift +1 -1
  73. package/Sources/Runic/Providers/Fireworks/FireworksProviderImplementation.swift +1 -1
  74. package/Sources/Runic/Providers/Gemini/GeminiProviderImplementation.swift +1 -1
  75. package/Sources/Runic/Providers/Groq/GroqProviderImplementation.swift +1 -1
  76. package/Sources/Runic/Providers/Kimi/KimiProviderImplementation.swift +1 -1
  77. package/Sources/Runic/Providers/MiniMax/MiniMaxProviderImplementation.swift +1 -1
  78. package/Sources/Runic/Providers/Mistral/MistralProviderImplementation.swift +1 -1
  79. package/Sources/Runic/Providers/OpenRouter/OpenRouterProviderImplementation.swift +1 -1
  80. package/Sources/Runic/Providers/Perplexity/PerplexityProviderImplementation.swift +1 -1
  81. package/Sources/Runic/Providers/SambaNova/SambaNovaProviderImplementation.swift +1 -1
  82. package/Sources/Runic/Providers/Shared/ProviderImplementation.swift +5 -2
  83. package/Sources/Runic/Providers/Shared/ProviderImplementationRegistry.swift +1 -1
  84. package/Sources/Runic/Providers/Shared/ProviderSettingsDescriptors.swift +1 -1
  85. package/Sources/Runic/Providers/Together/TogetherProviderImplementation.swift +1 -1
  86. package/Sources/Runic/Providers/XAI/XAIProviderImplementation.swift +1 -1
  87. package/Sources/Runic/Providers/Zai/ZaiProviderImplementation.swift +1 -1
  88. package/Sources/Runic/Resources/Fonts/Caveat-Bold.ttf +0 -0
  89. package/Sources/Runic/Resources/Fonts/Caveat-Regular.ttf +0 -0
  90. package/Sources/Runic/Resources/Fonts/FiraCode-Bold.ttf +0 -0
  91. package/Sources/Runic/Resources/Fonts/FiraCode-Light.ttf +0 -0
  92. package/Sources/Runic/Resources/Fonts/FiraCode-Medium.ttf +0 -0
  93. package/Sources/Runic/Resources/Fonts/FiraCode-Regular.ttf +0 -0
  94. package/Sources/Runic/Resources/Fonts/FiraCode-SemiBold.ttf +0 -0
  95. package/Sources/Runic/Resources/Fonts/IBMPlexMono-Bold.ttf +0 -0
  96. package/Sources/Runic/Resources/Fonts/IBMPlexMono-Medium.ttf +0 -0
  97. package/Sources/Runic/Resources/Fonts/IBMPlexMono-Regular.ttf +0 -0
  98. package/Sources/Runic/Resources/Fonts/IBMPlexMono-SemiBold.ttf +0 -0
  99. package/Sources/Runic/Resources/Fonts/Inconsolata-Bold.ttf +0 -0
  100. package/Sources/Runic/Resources/Fonts/Inconsolata-Medium.ttf +0 -0
  101. package/Sources/Runic/Resources/Fonts/Inconsolata-Regular.ttf +0 -0
  102. package/Sources/Runic/Resources/Fonts/Inconsolata-SemiBold.ttf +0 -0
  103. package/Sources/Runic/Resources/Fonts/JetBrainsMono-Bold.ttf +0 -0
  104. package/Sources/Runic/Resources/Fonts/JetBrainsMono-Medium.ttf +0 -0
  105. package/Sources/Runic/Resources/Fonts/JetBrainsMono-Regular.ttf +0 -0
  106. package/Sources/Runic/Resources/Fonts/JetBrainsMono-SemiBold.ttf +0 -0
  107. package/Sources/Runic/Resources/Fonts/SpaceMono-Bold.ttf +0 -0
  108. package/Sources/Runic/Resources/Fonts/SpaceMono-Regular.ttf +0 -0
  109. package/Sources/Runic/Resources/provider-context-windows.json +20 -0
  110. package/Sources/Runic/RunicApp.swift +46 -36
  111. package/Sources/Runic/SessionQuotaNotifications.swift +2 -2
  112. package/Sources/Runic/UsagePaceText.swift +1 -1
  113. package/Sources/Runic/UsageProgressBar.swift +13 -9
  114. package/Sources/Runic/Utilities/LoadingPattern.swift +4 -2
  115. package/Sources/Runic/Utilities/PerformanceConstants.swift +28 -28
  116. package/Sources/Runic/Views/Menu/AlertsMenuView.swift +19 -21
  117. package/Sources/Runic/Views/Menu/CostComparativeChartMenuView.swift +19 -19
  118. package/Sources/Runic/Views/Menu/CostHistoryChartMenuView.swift +7 -7
  119. package/Sources/Runic/Views/Menu/CreditsHistoryChartMenuView.swift +5 -5
  120. package/Sources/Runic/Views/Menu/EfficiencyMetricsMenuView.swift +29 -30
  121. package/Sources/Runic/Views/Menu/HeroStatCardsMenuView.swift +20 -17
  122. package/Sources/Runic/Views/Menu/HourlyActivityChartMenuView.swift +14 -14
  123. package/Sources/Runic/Views/Menu/InlineUsageChartView.swift +15 -20
  124. package/Sources/Runic/Views/Menu/MenuCardEntranceModifier.swift +3 -4
  125. package/Sources/Runic/Views/Menu/MenuCardSidebarView.swift +5 -25
  126. package/Sources/Runic/Views/Menu/MenuCardSkeletonView.swift +1 -1
  127. package/Sources/Runic/Views/Menu/MenuCardView.swift +120 -131
  128. package/Sources/Runic/Views/Menu/MenuContent.swift +11 -8
  129. package/Sources/Runic/Views/Menu/MenuDescriptor.swift +4 -2
  130. package/Sources/Runic/Views/Menu/MenuHighlightStyle.swift +1 -8
  131. package/Sources/Runic/Views/Menu/ModelBreakdownMenuView.swift +20 -14
  132. package/Sources/Runic/Views/Menu/OverviewMenuView.swift +198 -58
  133. package/Sources/Runic/Views/Menu/ProjectBreakdownMenuView.swift +16 -11
  134. package/Sources/Runic/Views/Menu/ProjectBudgetMenuView.swift +27 -27
  135. package/Sources/Runic/Views/Menu/ProviderTabBarView.swift +4 -6
  136. package/Sources/Runic/Views/Menu/SubscriptionUtilizationChartMenuView.swift +24 -11
  137. package/Sources/Runic/Views/Menu/UsageBreakdownChartMenuView.swift +5 -5
  138. package/Sources/Runic/Views/Menu/UsageHeatmapMenuView.swift +7 -8
  139. package/Sources/Runic/Views/Menu/UsageTimelineChartMenuView.swift +28 -76
  140. package/Sources/Runic/Views/Menu/UsageWindowComparisonChartMenuView.swift +8 -7
  141. package/Sources/Runic/Views/Menu/WeeklyActivityChartMenuView.swift +7 -7
  142. package/Sources/Runic/Views/Performance/PerformanceDashboardView.swift +50 -47
  143. package/Sources/Runic/Views/Preferences/LiquidComponents.swift +15 -14
  144. package/Sources/Runic/Views/Preferences/PreferencesAboutPane.swift +7 -7
  145. package/Sources/Runic/Views/Preferences/PreferencesAdvancedPane.swift +27 -27
  146. package/Sources/Runic/Views/Preferences/PreferencesAlertsPane.swift +38 -42
  147. package/Sources/Runic/Views/Preferences/PreferencesAnalyticsPane.swift +39 -43
  148. package/Sources/Runic/Views/Preferences/PreferencesBudgetsPane.swift +21 -21
  149. package/Sources/Runic/Views/Preferences/PreferencesComponents.swift +7 -7
  150. package/Sources/Runic/Views/Preferences/PreferencesCustomProvidersPane.swift +49 -50
  151. package/Sources/Runic/Views/Preferences/PreferencesDebugPane.swift +10 -10
  152. package/Sources/Runic/Views/Preferences/PreferencesGeneralPane.swift +40 -21
  153. package/Sources/Runic/Views/Preferences/PreferencesHelpPane.swift +7 -7
  154. package/Sources/Runic/Views/Preferences/PreferencesIntegrationsPane.swift +108 -33
  155. package/Sources/Runic/Views/Preferences/PreferencesPerformancePane.swift +26 -23
  156. package/Sources/Runic/Views/Preferences/PreferencesProvidersPane.swift +271 -226
  157. package/Sources/Runic/Views/Preferences/PreferencesSelection.swift +6 -2
  158. package/Sources/Runic/Views/Preferences/PreferencesSyncPane.swift +7 -5
  159. package/Sources/Runic/Views/Preferences/PreferencesView.swift +38 -31
  160. package/Sources/Runic/Views/Rating/QualityRatingView.swift +11 -11
  161. package/Sources/Runic/Views/Team/TeamDashboardView.swift +29 -24
  162. package/Sources/Runic/Views/Team/TeamInviteSheet.swift +16 -16
  163. package/Sources/Runic/Views/Team/TeamManagementView.swift +50 -49
  164. package/Sources/Runic/Views/Team/TeamMemberRow.swift +8 -8
  165. package/Sources/RunicCLI/CLIEntry.swift +83 -42
  166. package/Sources/RunicCLI/CLIRenderer.swift +1 -1
  167. package/Sources/RunicCLI/Commands/EnhancedUsageCommand.swift +60 -60
  168. package/Sources/RunicCLI/Commands/ModelsCommand.swift +45 -44
  169. package/Sources/RunicCLI/Commands/ProjectsCommand.swift +83 -69
  170. package/Sources/RunicCLI/Commands/ResetCommand.swift +50 -49
  171. package/Sources/RunicCLI/Commands/UsageLedgerSourceFactory.swift +31 -32
  172. package/Sources/RunicClaudeWebProbe/main.swift +1 -1
  173. package/Sources/RunicCore/AlertRuleStore.swift +20 -20
  174. package/Sources/RunicCore/BrowserCookieImportOrder.swift +3 -3
  175. package/Sources/RunicCore/CostUsageModels.swift +1 -1
  176. package/Sources/RunicCore/CustomProviders/CustomProviderModels.swift +29 -30
  177. package/Sources/RunicCore/CustomProviders/GenericProviderFetcher.swift +80 -71
  178. package/Sources/RunicCore/CustomProviders/ProviderTemplates.swift +53 -109
  179. package/Sources/RunicCore/Host/PTY/TTYCommandRunner.swift +3 -3
  180. package/Sources/RunicCore/Integrations/GitHubIntegration.swift +3 -3
  181. package/Sources/RunicCore/Logging/OSLogLogHandler.swift +6 -6
  182. package/Sources/RunicCore/Models/EnhancedUsageModels.swift +47 -48
  183. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardBrowserCookieImporter.swift +1 -1
  184. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardFetcher.swift +1 -1
  185. package/Sources/RunicCore/Performance/PerformanceModels.swift +4 -4
  186. package/Sources/RunicCore/Performance/PerformanceStorageImpl.swift +81 -64
  187. package/Sources/RunicCore/Policy/UsagePolicyEngine.swift +2 -2
  188. package/Sources/RunicCore/ProjectBudgetStore.swift +15 -12
  189. package/Sources/RunicCore/Providers/Antigravity/AntigravityProviderDescriptor.swift +4 -2
  190. package/Sources/RunicCore/Providers/Antigravity/AntigravityStatusProbe.swift +2 -2
  191. package/Sources/RunicCore/Providers/Auggie/AuggieProviderDescriptor.swift +2 -2
  192. package/Sources/RunicCore/Providers/Auggie/AuggieUsageFetcher.swift +3 -3
  193. package/Sources/RunicCore/Providers/Azure/AzureProviderDescriptor.swift +8 -8
  194. package/Sources/RunicCore/Providers/Azure/AzureUsageFetcher.swift +5 -3
  195. package/Sources/RunicCore/Providers/Bedrock/BedrockProviderDescriptor.swift +6 -6
  196. package/Sources/RunicCore/Providers/Bedrock/BedrockUsageFetcher.swift +3 -7
  197. package/Sources/RunicCore/Providers/Cerebras/CerebrasProviderDescriptor.swift +2 -2
  198. package/Sources/RunicCore/Providers/Cerebras/CerebrasUsageFetcher.swift +2 -2
  199. package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthUsageFetcher.swift +3 -3
  200. package/Sources/RunicCore/Providers/Claude/ClaudeProviderDescriptor.swift +4 -2
  201. package/Sources/RunicCore/Providers/Claude/ClaudeStatusProbe.swift +4 -4
  202. package/Sources/RunicCore/Providers/Claude/ClaudeUsageDataSource.swift +3 -1
  203. package/Sources/RunicCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift +1 -1
  204. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthCredentials.swift +1 -0
  205. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift +12 -1
  206. package/Sources/RunicCore/Providers/Codex/CodexProviderDescriptor.swift +4 -2
  207. package/Sources/RunicCore/Providers/Codex/CodexUsageDataSource.swift +3 -1
  208. package/Sources/RunicCore/Providers/Codex/CodexWebDashboardStrategy.swift +5 -3
  209. package/Sources/RunicCore/Providers/Cohere/CohereProviderDescriptor.swift +2 -2
  210. package/Sources/RunicCore/Providers/Cohere/CohereUsageFetcher.swift +3 -3
  211. package/Sources/RunicCore/Providers/Copilot/CopilotGitHubCLITokenReader.swift +7 -4
  212. package/Sources/RunicCore/Providers/Copilot/CopilotProviderDescriptor.swift +4 -4
  213. package/Sources/RunicCore/Providers/Copilot/CopilotUsageFetcher.swift +5 -3
  214. package/Sources/RunicCore/Providers/Copilot/CopilotVSCodeTokenReader.swift +9 -10
  215. package/Sources/RunicCore/Providers/Cursor/CursorProviderDescriptor.swift +4 -2
  216. package/Sources/RunicCore/Providers/Cursor/CursorStatusProbe.swift +1 -0
  217. package/Sources/RunicCore/Providers/DeepSeek/DeepSeekProviderDescriptor.swift +2 -2
  218. package/Sources/RunicCore/Providers/DeepSeek/DeepSeekUsageFetcher.swift +2 -2
  219. package/Sources/RunicCore/Providers/Factory/FactoryLocalStorageImporter.swift +4 -4
  220. package/Sources/RunicCore/Providers/Factory/FactoryProviderDescriptor.swift +4 -2
  221. package/Sources/RunicCore/Providers/Factory/FactoryStatusProbe.swift +3 -2
  222. package/Sources/RunicCore/Providers/Fireworks/FireworksProviderDescriptor.swift +2 -2
  223. package/Sources/RunicCore/Providers/Fireworks/FireworksUsageFetcher.swift +2 -2
  224. package/Sources/RunicCore/Providers/Gemini/GeminiProviderDescriptor.swift +4 -2
  225. package/Sources/RunicCore/Providers/Gemini/GeminiStatusProbe.swift +6 -2
  226. package/Sources/RunicCore/Providers/Groq/GroqProviderDescriptor.swift +2 -2
  227. package/Sources/RunicCore/Providers/Groq/GroqUsageFetcher.swift +2 -2
  228. package/Sources/RunicCore/Providers/Kimi/KimiProviderDescriptor.swift +2 -2
  229. package/Sources/RunicCore/Providers/Kimi/KimiUsageFetcher.swift +2 -2
  230. package/Sources/RunicCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift +10 -5
  231. package/Sources/RunicCore/Providers/MiniMax/MiniMaxUsageFetcher.swift +8 -11
  232. package/Sources/RunicCore/Providers/MiniMax/MiniMaxWebUsageFetcher.swift +10 -10
  233. package/Sources/RunicCore/Providers/Mistral/MistralProviderDescriptor.swift +2 -2
  234. package/Sources/RunicCore/Providers/Mistral/MistralUsageFetcher.swift +2 -2
  235. package/Sources/RunicCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift +2 -2
  236. package/Sources/RunicCore/Providers/OpenRouter/OpenRouterUsageFetcher.swift +25 -16
  237. package/Sources/RunicCore/Providers/Perplexity/PerplexityProviderDescriptor.swift +2 -2
  238. package/Sources/RunicCore/Providers/Perplexity/PerplexityUsageFetcher.swift +2 -2
  239. package/Sources/RunicCore/Providers/ProviderFetchPlan.swift +2 -2
  240. package/Sources/RunicCore/Providers/ProviderTokenResolver.swift +2 -2
  241. package/Sources/RunicCore/Providers/Qwen/QwenProviderDescriptor.swift +5 -3
  242. package/Sources/RunicCore/Providers/Qwen/QwenUsageStats.swift +27 -16
  243. package/Sources/RunicCore/Providers/SambaNova/SambaNovaProviderDescriptor.swift +2 -2
  244. package/Sources/RunicCore/Providers/SambaNova/SambaNovaUsageFetcher.swift +2 -2
  245. package/Sources/RunicCore/Providers/Together/TogetherProviderDescriptor.swift +2 -2
  246. package/Sources/RunicCore/Providers/Together/TogetherUsageFetcher.swift +2 -2
  247. package/Sources/RunicCore/Providers/VertexAI/VertexAIProviderDescriptor.swift +5 -5
  248. package/Sources/RunicCore/Providers/VertexAI/VertexAISettingsReader.swift +1 -1
  249. package/Sources/RunicCore/Providers/VertexAI/VertexAIUsageFetcher.swift +3 -3
  250. package/Sources/RunicCore/Providers/XAI/XAIProviderDescriptor.swift +2 -2
  251. package/Sources/RunicCore/Providers/XAI/XAIUsageFetcher.swift +2 -2
  252. package/Sources/RunicCore/Providers/Zai/ZaiProviderDescriptor.swift +4 -2
  253. package/Sources/RunicCore/Providers/Zai/ZaiUsageStats.swift +51 -30
  254. package/Sources/RunicCore/Sync/BackgroundSyncManager.swift +76 -77
  255. package/Sources/RunicCore/Sync/SyncConflictResolver.swift +55 -58
  256. package/Sources/RunicCore/Sync/SyncProtocol.swift +21 -22
  257. package/Sources/RunicCore/Sync/SyncRecord.swift +46 -49
  258. package/Sources/RunicCore/Sync/SyncUsageExample.swift +44 -50
  259. package/Sources/RunicCore/Sync/TeamSyncRecords.swift +94 -101
  260. package/Sources/RunicCore/Sync/iCloudSyncEngine.swift +51 -46
  261. package/Sources/RunicCore/Team/TeamModels.swift +111 -110
  262. package/Sources/RunicCore/Team/TeamStore.swift +126 -109
  263. package/Sources/RunicCore/UsageFetcher.swift +2 -2
  264. package/Sources/RunicCore/UsageFormatter.swift +25 -25
  265. package/Sources/RunicCore/UsageLedger/ClaudeUsageLogSource.swift +141 -29
  266. package/Sources/RunicCore/UsageLedger/CodexUsageLogSource.swift +59 -5
  267. package/Sources/RunicCore/UsageLedger/LedgerCache.swift +161 -0
  268. package/Sources/RunicCore/UsageLedger/OTelGenAILedgerAdapter.swift +1 -1
  269. package/Sources/RunicCore/UsageLedger/UsageLedgerAggregator.swift +40 -25
  270. package/Sources/RunicCore/UsageLedger/UsageLedgerInsightsAdvisor.swift +1 -1
  271. package/Sources/RunicCore/UsageLedger/UsageLedgerProjectIdentity.swift +55 -16
  272. package/Sources/RunicCore/Vendored/CostUsage/CostUsageCache.swift +6 -6
  273. package/Sources/RunicCore/Vendored/CostUsage/CostUsageJsonl.swift +1 -1
  274. package/Sources/RunicCore/Vendored/CostUsage/CostUsagePricing.swift +2 -2
  275. package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner.swift +5 -5
  276. package/Tests/RunicTests/AppDelegateTests.swift +8 -3
  277. package/Tests/RunicTests/BrowserCookieOrderLabelTests.swift +2 -3
  278. package/Tests/RunicTests/CLIArgumentParsingTests.swift +3 -4
  279. package/Tests/RunicTests/CLICostTests.swift +6 -7
  280. package/Tests/RunicTests/CLIProviderSelectionTests.swift +2 -3
  281. package/Tests/RunicTests/CLISnapshotTests.swift +3 -4
  282. package/Tests/RunicTests/CLIWebFallbackTests.swift +3 -4
  283. package/Tests/RunicTests/ClaudeOAuthTests.swift +11 -12
  284. package/Tests/RunicTests/ClaudeResilienceTests.swift +3 -4
  285. package/Tests/RunicTests/ClaudeUsageTests.swift +20 -21
  286. package/Tests/RunicTests/CodexOAuthTests.swift +7 -8
  287. package/Tests/RunicTests/CodexUsageLogSourceTests.swift +1 -3
  288. package/Tests/RunicTests/CopilotTokenResolverTests.swift +5 -6
  289. package/Tests/RunicTests/CopilotVSCodeTokenReaderTests.swift +4 -5
  290. package/Tests/RunicTests/CostUsageDecodingTests.swift +15 -17
  291. package/Tests/RunicTests/CostUsagePricingTests.swift +5 -7
  292. package/Tests/RunicTests/CostUsageScannerTests.swift +10 -12
  293. package/Tests/RunicTests/CursorStatusProbeTests.swift +15 -16
  294. package/Tests/RunicTests/FactoryStatusProbeTests.swift +3 -5
  295. package/Tests/RunicTests/GeminiLoginAlertTests.swift +3 -4
  296. package/Tests/RunicTests/GeminiStatusProbeAPITests.swift +11 -11
  297. package/Tests/RunicTests/GeminiStatusProbePlanTests.swift +8 -8
  298. package/Tests/RunicTests/GeminiStatusProbeTests.swift +20 -21
  299. package/Tests/RunicTests/GoogleWorkspaceStatusTests.swift +2 -3
  300. package/Tests/RunicTests/InstallOriginTests.swift +1 -2
  301. package/Tests/RunicTests/KeyboardShortcutsBundleTests.swift +2 -2
  302. package/Tests/RunicTests/LiveAccountTests.swift +3 -3
  303. package/Tests/RunicTests/LoadingPatternTests.swift +3 -4
  304. package/Tests/RunicTests/MenuCardModelTests.swift +33 -34
  305. package/Tests/RunicTests/MiniMaxCookieHeaderStoreTestSupport.swift +4 -1
  306. package/Tests/RunicTests/MiniMaxGroupIDStoreTestSupport.swift +4 -1
  307. package/Tests/RunicTests/MiniMaxParsingTests.swift +4 -5
  308. package/Tests/RunicTests/MiniMaxTokenStoreTestSupport.swift +4 -1
  309. package/Tests/RunicTests/OTelGenAILedgerAdapterTests.swift +4 -6
  310. package/Tests/RunicTests/OpenAIDashboardBrowserCookieImporterTests.swift +1 -2
  311. package/Tests/RunicTests/OpenAIDashboardFetcherCreditsWaitTests.swift +5 -6
  312. package/Tests/RunicTests/OpenAIDashboardOffscreenHostTests.swift +2 -3
  313. package/Tests/RunicTests/OpenAIDashboardParserTests.swift +12 -13
  314. package/Tests/RunicTests/OpenAIWebAccountSwitchTests.swift +17 -6
  315. package/Tests/RunicTests/OpenRouterTokenStoreTestSupport.swift +52 -13
  316. package/Tests/RunicTests/PathBuilderTests.swift +14 -15
  317. package/Tests/RunicTests/ProviderFetchPipelineTests.swift +3 -4
  318. package/Tests/RunicTests/ProviderIconResourcesTests.swift +1 -2
  319. package/Tests/RunicTests/ProviderMetadataStatusLinkTests.swift +1 -2
  320. package/Tests/RunicTests/ProviderSettingsDescriptorTests.swift +53 -19
  321. package/Tests/RunicTests/ProviderToggleStoreTests.swift +10 -11
  322. package/Tests/RunicTests/RunicTests.swift +6 -7
  323. package/Tests/RunicTests/SessionQuotaNotificationLogicTests.swift +5 -6
  324. package/Tests/RunicTests/SettingsStoreTests.swift +149 -45
  325. package/Tests/RunicTests/StatusItemAnimationTests.swift +8 -3
  326. package/Tests/RunicTests/StatusMenuTests.swift +63 -22
  327. package/Tests/RunicTests/StatusProbeTests.swift +28 -29
  328. package/Tests/RunicTests/SubprocessRunnerTests.swift +1 -3
  329. package/Tests/RunicTests/SubscriptionDetectionTests.swift +8 -9
  330. package/Tests/RunicTests/TTYCommandRunnerTests.swift +8 -9
  331. package/Tests/RunicTests/TeamShowbackExporterTests.swift +2 -4
  332. package/Tests/RunicTests/TextParsingTests.swift +1 -2
  333. package/Tests/RunicTests/UsageFormatterTests.swift +15 -16
  334. package/Tests/RunicTests/UsageLedgerAnomalyDetectionTests.swift +12 -14
  335. package/Tests/RunicTests/UsageLedgerForecastTests.swift +4 -6
  336. package/Tests/RunicTests/UsageLedgerInsightsAdvisorTests.swift +7 -9
  337. package/Tests/RunicTests/UsageLedgerProjectNameTests.swift +6 -8
  338. package/Tests/RunicTests/UsagePaceTests.swift +5 -6
  339. package/Tests/RunicTests/UsagePaceTextTests.swift +8 -9
  340. package/Tests/RunicTests/UsagePolicyEngineTests.swift +3 -5
  341. package/Tests/RunicTests/WidgetSnapshotTests.swift +1 -3
  342. package/Tests/RunicTests/ZaiProviderTests.swift +3 -5
  343. package/Tests/RunicTests/ZaiTokenStoreTestSupport.swift +4 -1
  344. package/appcast.xml +36 -37
  345. package/package.json +1 -1
  346. package/runic.png +0 -0
  347. package/runic.svg +15 -0
  348. package/version.env +2 -2
@@ -1,48 +1,48 @@
1
1
  import AppKit
2
- import RunicCore
2
+ import CoreGraphics
3
3
  import Foundation
4
4
  import Observation
5
+ import RunicCore
5
6
  import Silo
6
- import CoreGraphics
7
7
 
8
- /// **UsageStore** - Main state management for AI provider usage tracking
9
- ///
10
- /// **Purpose:**
11
- /// Central store managing all provider usage data, snapshots, errors, and refresh logic.
12
- /// This is the single source of truth for the app's state.
13
- ///
14
- /// **Responsibilities:**
15
- /// - Fetch usage snapshots from all providers (Claude, Codex, Cursor, Gemini, etc.)
16
- /// - Manage authentication state and cookies via Silo
17
- /// - Track token usage across all providers
18
- /// - Coordinate "ping" operations (manual and automatic)
19
- /// - Handle errors and stale data detection
20
- /// - Provide Observable state for SwiftUI views
21
- ///
22
- /// **Performance Philosophy:**
23
- /// - **Zero Token Leakage** - Always use cookies/cached data first before API calls
24
- /// - **Single Ping Strategy** - One ping per provider per session, cache rest
25
- /// - **Stale Detection** - Mark data stale after 5 minutes (PerformanceConstants.staleDuration)
26
- ///
27
- /// **Known Issues:**
28
- /// - **God Class** - 2089 lines, needs refactoring (see PLAN.md)
29
- /// - Planned split: `UsageStateStore`, `UsageFetchingActor`, `TokenUsageService`
30
- ///
31
- /// **Dependencies:**
32
- /// - `RunicCore` - Provider descriptors and status probes
33
- /// - `Silo` - Browser cookie access
34
- /// - `Observation` - SwiftUI state updates
35
- ///
36
- /// **Usage:**
37
- /// ```swift
38
- /// @Observable
39
- /// final class UsageStore {
40
- /// var snapshots: [UsageProvider: ProviderSnapshot] = [:]
41
- /// var errors: [UsageProvider: Error] = [:]
42
- ///
43
- /// func ping(provider: UsageProvider) async { ... }
44
- /// }
45
- /// ```
8
+ // **UsageStore** - Main state management for AI provider usage tracking
9
+ //
10
+ // **Purpose:**
11
+ // Central store managing all provider usage data, snapshots, errors, and refresh logic.
12
+ // This is the single source of truth for the app's state.
13
+ //
14
+ // **Responsibilities:**
15
+ // - Fetch usage snapshots from all providers (Claude, Codex, Cursor, Gemini, etc.)
16
+ // - Manage authentication state and cookies via Silo
17
+ // - Track token usage across all providers
18
+ // - Coordinate "ping" operations (manual and automatic)
19
+ // - Handle errors and stale data detection
20
+ // - Provide Observable state for SwiftUI views
21
+ //
22
+ // **Performance Philosophy:**
23
+ // - **Zero Token Leakage** - Always use cookies/cached data first before API calls
24
+ // - **Single Ping Strategy** - One ping per provider per session, cache rest
25
+ // - **Stale Detection** - Mark data stale after 5 minutes (PerformanceConstants.staleDuration)
26
+ //
27
+ // **Known Issues:**
28
+ // - **God Class** - 2089 lines, needs refactoring (see PLAN.md)
29
+ // - Planned split: `UsageStateStore`, `UsageFetchingActor`, `TokenUsageService`
30
+ //
31
+ // **Dependencies:**
32
+ // - `RunicCore` - Provider descriptors and status probes
33
+ // - `Silo` - Browser cookie access
34
+ // - `Observation` - SwiftUI state updates
35
+ //
36
+ // **Usage:**
37
+ // ```swift
38
+ // @Observable
39
+ // final class UsageStore {
40
+ // var snapshots: [UsageProvider: ProviderSnapshot] = [:]
41
+ // var errors: [UsageProvider: Error] = [:]
42
+ //
43
+ // func ping(provider: UsageProvider) async { ... }
44
+ // }
45
+ // ```
46
46
 
47
47
  // MARK: - Observation helpers
48
48
 
@@ -305,8 +305,8 @@ struct ConsecutiveFailureGate {
305
305
  }
306
306
  }
307
307
 
308
- struct UsageLedgerAnomalySummary: Sendable, Hashable {
309
- enum Severity: Int, Sendable, Hashable {
308
+ struct UsageLedgerAnomalySummary: Hashable {
309
+ enum Severity: Int, Hashable {
310
310
  case elevated = 1
311
311
  case high = 2
312
312
  case critical = 3
@@ -320,8 +320,8 @@ struct UsageLedgerAnomalySummary: Sendable, Hashable {
320
320
  }
321
321
  }
322
322
 
323
- struct MetricAnomaly: Sendable, Hashable {
324
- enum Metric: String, Sendable, Hashable {
323
+ struct MetricAnomaly: Hashable {
324
+ enum Metric: String, Hashable {
325
325
  case tokens
326
326
  case spend
327
327
 
@@ -340,8 +340,8 @@ struct UsageLedgerAnomalySummary: Sendable, Hashable {
340
340
  let percentIncrease: Double
341
341
  }
342
342
 
343
- struct Explanation: Sendable, Hashable {
344
- struct Factor: Sendable, Hashable {
343
+ struct Explanation: Hashable {
344
+ struct Factor: Hashable {
345
345
  let metric: MetricAnomaly.Metric
346
346
  let severity: Severity
347
347
  let percentIncrease: Double
@@ -353,6 +353,7 @@ struct UsageLedgerAnomalySummary: Sendable, Hashable {
353
353
  let primaryFactor: Factor
354
354
  let contributingFactors: [Factor]
355
355
  }
356
+
356
357
  let provider: UsageProvider
357
358
  let baselineDays: Int
358
359
  let tokenAnomaly: MetricAnomaly?
@@ -360,7 +361,7 @@ struct UsageLedgerAnomalySummary: Sendable, Hashable {
360
361
 
361
362
  var primaryAnomaly: MetricAnomaly? {
362
363
  [self.tokenAnomaly, self.spendAnomaly]
363
- .compactMap { $0 }
364
+ .compactMap(\.self)
364
365
  .max { lhs, rhs in
365
366
  if lhs.severity != rhs.severity {
366
367
  return lhs.severity.rawValue < rhs.severity.rawValue
@@ -370,11 +371,11 @@ struct UsageLedgerAnomalySummary: Sendable, Hashable {
370
371
  }
371
372
 
372
373
  func secondaryAnomaly(excluding metric: MetricAnomaly.Metric) -> MetricAnomaly? {
373
- let candidates = [self.tokenAnomaly, self.spendAnomaly].compactMap { $0 }
374
+ let candidates = [self.tokenAnomaly, self.spendAnomaly].compactMap(\.self)
374
375
  guard !candidates.isEmpty else { return nil }
375
376
  return candidates.first { $0.metric != metric }
376
377
  }
377
-
378
+
378
379
  var explanation: Explanation? {
379
380
  guard let primary = self.primaryAnomaly else { return nil }
380
381
  let primaryFactor = self.factor(from: primary)
@@ -522,7 +523,7 @@ enum UsageLedgerAnomalyDetector {
522
523
  }
523
524
  }
524
525
 
525
- struct ProviderHistoryDaySnapshot: Sendable, Hashable, Identifiable {
526
+ struct ProviderHistoryDaySnapshot: Hashable, Identifiable {
526
527
  let dayStart: Date
527
528
  let totals: UsageLedgerTotals
528
529
  let requestCount: Int
@@ -532,10 +533,12 @@ struct ProviderHistoryDaySnapshot: Sendable, Hashable, Identifiable {
532
533
  let modelSummaries: [UsageLedgerModelSummary]
533
534
  let projectSummaries: [UsageLedgerProjectSummary]
534
535
 
535
- var id: Date { self.dayStart }
536
+ var id: Date {
537
+ self.dayStart
538
+ }
536
539
  }
537
540
 
538
- struct ProviderHistoryMonthSnapshot: Sendable, Hashable {
541
+ struct ProviderHistoryMonthSnapshot: Hashable {
539
542
  let provider: UsageProvider
540
543
  let monthStart: Date
541
544
  let generatedAt: Date
@@ -544,6 +547,7 @@ struct ProviderHistoryMonthSnapshot: Sendable, Hashable {
544
547
  let note: String?
545
548
  let error: String?
546
549
  }
550
+
547
551
  @MainActor
548
552
  @Observable
549
553
  final class UsageStore {
@@ -629,16 +633,22 @@ final class UsageStore {
629
633
  @ObservationIgnored private var tokenTimerTask: Task<Void, Never>?
630
634
  @ObservationIgnored private var tokenRefreshSequenceTask: Task<Void, Never>?
631
635
  @ObservationIgnored private var ledgerRefreshTask: Task<Void, Never>?
632
- @ObservationIgnored private var providerHistoryMonthCache: [UsageProvider: [String: ProviderHistoryMonthCacheEntry]] = [:]
636
+ @ObservationIgnored private var providerHistoryMonthCache: [
637
+ UsageProvider: [String: ProviderHistoryMonthCacheEntry]
638
+ ] =
639
+ [:]
633
640
  @ObservationIgnored private var lastKnownSessionRemaining: [UsageProvider: Double] = [:]
634
641
  @ObservationIgnored private(set) var lastTokenFetchAt: [UsageProvider: Date] = [:]
635
642
  @ObservationIgnored private let tokenFetchTTL: TimeInterval = 60 * 60
636
643
  @ObservationIgnored private let tokenFetchTimeout: TimeInterval = 10 * 60
637
644
  @ObservationIgnored private let ledgerRefreshTTL: TimeInterval = 90
638
- @ObservationIgnored private let ledgerMaxAgeDays: Int = 3
645
+ @ObservationIgnored private var ledgerMaxAgeDays: Int {
646
+ self.settings.ledgerMaxAgeDays
647
+ }
648
+
639
649
  @ObservationIgnored private let providerHistoryCacheTTL: TimeInterval = 90
640
650
  @ObservationIgnored private let providerHistoryMaxScanDays: Int = 180
641
- @ObservationIgnored nonisolated(unsafe) private let performanceStorage: PerformanceStorageImpl?
651
+ @ObservationIgnored private nonisolated(unsafe) let performanceStorage: PerformanceStorageImpl?
642
652
 
643
653
  init(
644
654
  fetcher: UsageFetcher,
@@ -928,7 +938,7 @@ final class UsageStore {
928
938
  func ledgerProjectBreakdown(for provider: UsageProvider) -> [UsageLedgerProjectSummary] {
929
939
  self.ledgerProjectBreakdowns[provider] ?? []
930
940
  }
931
-
941
+
932
942
  func ledgerSpendForecast(for provider: UsageProvider) -> UsageLedgerSpendForecast? {
933
943
  self.ledgerSpendForecasts[provider]
934
944
  }
@@ -945,7 +955,6 @@ final class UsageStore {
945
955
  self.ledgerAnomalies[provider]
946
956
  }
947
957
 
948
-
949
958
  func ledgerError(for provider: UsageProvider) -> String? {
950
959
  self.ledgerErrors[provider]
951
960
  }
@@ -1084,27 +1093,27 @@ final class UsageStore {
1084
1093
  case .codex:
1085
1094
  return CodexUsageLogSource(maxAgeDays: maxAgeDays, now: now)
1086
1095
  case .copilot,
1087
- .gemini,
1088
- .antigravity,
1089
- .cursor,
1090
- .factory,
1091
- .zai,
1092
- .minimax,
1093
- .openrouter,
1094
- .groq,
1095
- .deepseek,
1096
- .fireworks,
1097
- .mistral,
1098
- .perplexity,
1099
- .kimi,
1100
- .auggie,
1101
- .together,
1102
- .cohere,
1103
- .xai,
1104
- .cerebras,
1105
- .sambanova,
1106
- .azure,
1107
- .bedrock:
1096
+ .gemini,
1097
+ .antigravity,
1098
+ .cursor,
1099
+ .factory,
1100
+ .zai,
1101
+ .minimax,
1102
+ .openrouter,
1103
+ .groq,
1104
+ .deepseek,
1105
+ .fireworks,
1106
+ .mistral,
1107
+ .perplexity,
1108
+ .kimi,
1109
+ .auggie,
1110
+ .together,
1111
+ .cohere,
1112
+ .xai,
1113
+ .cerebras,
1114
+ .sambanova,
1115
+ .azure,
1116
+ .bedrock:
1108
1117
  return self.providerOTelHistorySource(provider: provider, now: now, maxAgeDays: maxAgeDays)
1109
1118
  @unknown default:
1110
1119
  return nil
@@ -1131,7 +1140,7 @@ final class UsageStore {
1131
1140
  minTimestamp: cutoff)
1132
1141
  }
1133
1142
 
1134
- private struct UsageLedgerProviderFilterSource: UsageLedgerSource, Sendable {
1143
+ private struct UsageLedgerProviderFilterSource: UsageLedgerSource {
1135
1144
  private let source: any UsageLedgerSource
1136
1145
  private let provider: UsageProvider
1137
1146
  private let minTimestamp: Date?
@@ -1165,7 +1174,7 @@ final class UsageStore {
1165
1174
  Self.splitPathList(env["RUNIC_\(providerKey)_OTEL_GENAI_LOG_PATH"]),
1166
1175
  Self.splitPathList(env["RUNIC_\(providerKey)_OTEL_LOG_PATHS"]),
1167
1176
  Self.splitPathList(env["RUNIC_\(providerKey)_OTEL_LOG_PATH"]),
1168
- ].flatMap { $0 }
1177
+ ].flatMap(\.self)
1169
1178
 
1170
1179
  let urls = candidatePaths.compactMap(Self.expandTildePath)
1171
1180
  return Self.discoverOTelLedgerFiles(from: urls)
@@ -1250,7 +1259,7 @@ final class UsageStore {
1250
1259
  error: nil)
1251
1260
  }
1252
1261
 
1253
- nonisolated private static func providerHistoryDays(
1262
+ private nonisolated static func providerHistoryDays(
1254
1263
  entries: [UsageLedgerEntry],
1255
1264
  timeZone: TimeZone) -> [ProviderHistoryDaySnapshot]
1256
1265
  {
@@ -1289,6 +1298,15 @@ final class UsageStore {
1289
1298
  }
1290
1299
  }
1291
1300
 
1301
+ /// Refresh a single provider (used when user clicks Ping on a specific provider tab).
1302
+ func refreshSingleProvider(_ provider: UsageProvider) async {
1303
+ self.isRefreshing = true
1304
+ defer { self.isRefreshing = false }
1305
+ await self.refreshProvider(provider, trigger: .manual)
1306
+ await self.refreshStatus(provider, trigger: .manual)
1307
+ self.scheduleLedgerRefresh(force: true, inactiveProviders: [])
1308
+ }
1309
+
1292
1310
  func refresh(trigger: RefreshTrigger = .manual, forceTokenUsage: Bool = false) async {
1293
1311
  guard !self.isRefreshing else { return }
1294
1312
  let now = Date()
@@ -1332,7 +1350,7 @@ final class UsageStore {
1332
1350
  await self.refreshCreditsIfNeeded()
1333
1351
  }
1334
1352
 
1335
- self.scheduleLedgerRefresh(force: forceTokenUsage, inactiveProviders: inactiveProviders)
1353
+ self.scheduleLedgerRefresh(force: true, inactiveProviders: inactiveProviders)
1336
1354
 
1337
1355
  // Refresh custom providers
1338
1356
  await self.refreshCustomProviders()
@@ -1368,12 +1386,11 @@ final class UsageStore {
1368
1386
 
1369
1387
  // Track successful latency
1370
1388
  await self.trackLatency(
1371
- provider: .codex, // TODO: Add custom provider tracking
1389
+ provider: .codex, // TODO: Add custom provider tracking
1372
1390
  requestID: requestID,
1373
1391
  startTime: startTime,
1374
1392
  endTime: endTime,
1375
- success: true
1376
- )
1393
+ success: true)
1377
1394
 
1378
1395
  let snapshot = CustomProviderSnapshot.from(usageData: usageData.toCustomUsageData(), config: config)
1379
1396
  await MainActor.run {
@@ -1385,13 +1402,12 @@ final class UsageStore {
1385
1402
 
1386
1403
  // Track failed latency and error
1387
1404
  await self.trackLatency(
1388
- provider: .codex, // TODO: Add custom provider tracking
1405
+ provider: .codex, // TODO: Add custom provider tracking
1389
1406
  requestID: requestID,
1390
1407
  startTime: startTime,
1391
1408
  endTime: endTime,
1392
- success: false
1393
- )
1394
- await self.trackError(provider: .codex, error: error) // TODO: Add custom provider tracking
1409
+ success: false)
1410
+ await self.trackError(provider: .codex, error: error) // TODO: Add custom provider tracking
1395
1411
 
1396
1412
  await MainActor.run {
1397
1413
  self.customProviderErrors[id] = error.localizedDescription
@@ -1525,11 +1541,11 @@ final class UsageStore {
1525
1541
  private func systemPauseDetailText(_ reason: AutoRefreshSuspensionReason) -> String {
1526
1542
  switch reason {
1527
1543
  case .systemSleep:
1528
- return "your Mac went to sleep"
1544
+ "your Mac went to sleep"
1529
1545
  case .screenSleep:
1530
- return "the display went to sleep"
1546
+ "the display went to sleep"
1531
1547
  case .sessionInactive:
1532
- return "your session became inactive"
1548
+ "your session became inactive"
1533
1549
  }
1534
1550
  }
1535
1551
 
@@ -1619,11 +1635,11 @@ final class UsageStore {
1619
1635
  func windowChanged(_ lhs: RateWindow?, _ rhs: RateWindow?) -> Bool {
1620
1636
  switch (lhs, rhs) {
1621
1637
  case (nil, nil):
1622
- return false
1638
+ false
1623
1639
  case let (l?, r?):
1624
- return abs(l.usedPercent - r.usedPercent) >= 0.1
1640
+ abs(l.usedPercent - r.usedPercent) >= 0.1
1625
1641
  case (nil, _), (_, nil):
1626
- return true
1642
+ true
1627
1643
  }
1628
1644
  }
1629
1645
 
@@ -1660,6 +1676,37 @@ final class UsageStore {
1660
1676
  }
1661
1677
  if self.ledgerRefreshTask != nil { return }
1662
1678
 
1679
+ // Immediately populate from cache so the UI has data before the background scan.
1680
+ let providersToCache = providers
1681
+ .filter { self.ledgerAllDailySummaries[$0] == nil || self.ledgerAllDailySummaries[$0]?.isEmpty == true }
1682
+ if !providersToCache.isEmpty {
1683
+ Task {
1684
+ let cache = LedgerCache.shared
1685
+ for provider in providersToCache {
1686
+ let providerKey = provider.rawValue
1687
+ guard let cached = await cache.loadCachedDailies(provider: providerKey) else { continue }
1688
+ let summaries = cached.dailies.compactMap { $0.toLedgerDailySummary(provider: provider) }
1689
+ if !summaries.isEmpty {
1690
+ await MainActor.run { [weak self] in
1691
+ guard let self else { return }
1692
+ if self.ledgerAllDailySummaries[provider] == nil || self.ledgerAllDailySummaries[provider]?
1693
+ .isEmpty == true
1694
+ {
1695
+ self.ledgerAllDailySummaries[provider] = summaries
1696
+ // Also populate today's summary from cache.
1697
+ let todayStart = Calendar.current.startOfDay(for: now)
1698
+ if self.ledgerDailySummaries[provider] == nil,
1699
+ let todaySummary = summaries.first(where: { $0.dayStart == todayStart })
1700
+ {
1701
+ self.ledgerDailySummaries[provider] = todaySummary
1702
+ }
1703
+ }
1704
+ }
1705
+ }
1706
+ }
1707
+ }
1708
+ }
1709
+
1663
1710
  self.ledgerRefreshTask = Task.detached(priority: .utility) { [weak self] in
1664
1711
  guard let self else { return }
1665
1712
  let result = await self.loadLedgerInsights(sources: sources, now: now)
@@ -1783,13 +1830,17 @@ final class UsageStore {
1783
1830
  self?.tokenRefreshSequenceTask = nil
1784
1831
  }
1785
1832
  }
1786
- for provider in UsageProvider.allCases {
1787
- if Task.isCancelled { break }
1788
- await self.refreshTokenUsage(
1789
- provider,
1790
- force: force,
1791
- trigger: trigger,
1792
- inactiveProviders: inactiveProviders)
1833
+ await withTaskGroup(of: Void.self) { group in
1834
+ for provider in UsageProvider.allCases {
1835
+ group.addTask {
1836
+ guard !Task.isCancelled else { return }
1837
+ await self.refreshTokenUsage(
1838
+ provider,
1839
+ force: force,
1840
+ trigger: trigger,
1841
+ inactiveProviders: inactiveProviders)
1842
+ }
1843
+ }
1793
1844
  }
1794
1845
  }
1795
1846
  }
@@ -1801,7 +1852,7 @@ final class UsageStore {
1801
1852
  self.ledgerRefreshTask?.cancel()
1802
1853
  }
1803
1854
 
1804
- private struct LedgerRefreshResult: Sendable {
1855
+ private struct LedgerRefreshResult {
1805
1856
  let dailyByProvider: [UsageProvider: UsageLedgerDailySummary]
1806
1857
  let allDailySummariesByProvider: [UsageProvider: [UsageLedgerDailySummary]]
1807
1858
  let hourlySummariesByProvider: [UsageProvider: [UsageLedgerHourlySummary]]
@@ -1829,7 +1880,7 @@ final class UsageStore {
1829
1880
  now: Date,
1830
1881
  inactiveProviders: Set<UsageProvider>) -> [(UsageProvider, any UsageLedgerSource)]
1831
1882
  {
1832
- return UsageProvider.allCases.compactMap { provider in
1883
+ UsageProvider.allCases.compactMap { provider in
1833
1884
  guard self.isEnabled(provider), !inactiveProviders.contains(provider) else { return nil }
1834
1885
  guard let source = self.providerHistorySource(
1835
1886
  provider: provider,
@@ -1845,23 +1896,23 @@ final class UsageStore {
1845
1896
  now: Date) async -> LedgerRefreshResult
1846
1897
  {
1847
1898
  guard !sources.isEmpty else {
1848
- return LedgerRefreshResult(
1849
- dailyByProvider: [:],
1850
- allDailySummariesByProvider: [:],
1851
- hourlySummariesByProvider: [:],
1852
- activeBlocksByProvider: [:],
1853
- topModelsByProvider: [:],
1854
- topProjectsByProvider: [:],
1855
- modelBreakdownsByProvider: [:],
1856
- projectBreakdownsByProvider: [:],
1857
- spendForecastsByProvider: [:],
1858
- projectSpendForecastsByProvider: [:],
1859
- topProjectSpendForecastsByProvider: [:],
1860
- anomaliesByProvider: [:],
1861
- errorsByProvider: [:],
1862
- lastActivityByProvider: [:],
1863
- updatedAt: now,
1864
- providers: [])
1899
+ return LedgerRefreshResult(
1900
+ dailyByProvider: [:],
1901
+ allDailySummariesByProvider: [:],
1902
+ hourlySummariesByProvider: [:],
1903
+ activeBlocksByProvider: [:],
1904
+ topModelsByProvider: [:],
1905
+ topProjectsByProvider: [:],
1906
+ modelBreakdownsByProvider: [:],
1907
+ projectBreakdownsByProvider: [:],
1908
+ spendForecastsByProvider: [:],
1909
+ projectSpendForecastsByProvider: [:],
1910
+ topProjectSpendForecastsByProvider: [:],
1911
+ anomaliesByProvider: [:],
1912
+ errorsByProvider: [:],
1913
+ lastActivityByProvider: [:],
1914
+ updatedAt: now,
1915
+ providers: [])
1865
1916
  }
1866
1917
 
1867
1918
  let providers = sources.map(\.0)
@@ -1965,10 +2016,10 @@ final class UsageStore {
1965
2016
  projectBreakdownsByProvider[summary.provider, default: []].append(summary)
1966
2017
  }
1967
2018
 
1968
- let providerForecasts = UsageLedgerAggregator.providerSpendForecasts(
1969
- entries: entries,
1970
- now: now,
1971
- timeZone: timeZone)
2019
+ let providerForecasts = UsageLedgerAggregator.providerSpendForecasts(
2020
+ entries: entries,
2021
+ now: now,
2022
+ timeZone: timeZone)
1972
2023
  var spendForecastsByProvider: [UsageProvider: UsageLedgerSpendForecast] = [:]
1973
2024
  for forecast in providerForecasts where spendForecastsByProvider[forecast.provider] == nil {
1974
2025
  spendForecastsByProvider[forecast.provider] = forecast
@@ -1991,34 +2042,34 @@ final class UsageStore {
1991
2042
  }
1992
2043
 
1993
2044
  var topProjectSpendForecastsByProvider: [UsageProvider: UsageLedgerSpendForecast] = [:]
1994
- for (provider, summary) in topProjectsByProvider {
1995
- guard let forecasts = projectSpendForecastsByProvider[provider] else { continue }
1996
- if let matched = self.matchingProjectForecast(for: summary, forecasts: forecasts) {
1997
- topProjectSpendForecastsByProvider[provider] = matched
2045
+ for (provider, summary) in topProjectsByProvider {
2046
+ guard let forecasts = projectSpendForecastsByProvider[provider] else { continue }
2047
+ if let matched = self.matchingProjectForecast(for: summary, forecasts: forecasts) {
2048
+ topProjectSpendForecastsByProvider[provider] = matched
2049
+ }
1998
2050
  }
1999
- }
2000
- let anomaliesByProvider = UsageLedgerAnomalyDetector.summaries(
2001
- dailySummaries: dailySummaries,
2002
- now: now,
2003
- calendar: calendar)
2051
+ let anomaliesByProvider = UsageLedgerAnomalyDetector.summaries(
2052
+ dailySummaries: dailySummaries,
2053
+ now: now,
2054
+ calendar: calendar)
2004
2055
 
2005
2056
  return LedgerRefreshResult(
2006
2057
  dailyByProvider: dailyByProvider,
2007
2058
  allDailySummariesByProvider: allDailySummariesByProvider,
2008
2059
  hourlySummariesByProvider: hourlySummariesByProvider,
2009
- activeBlocksByProvider: activeByProvider,
2010
- topModelsByProvider: topModelsByProvider,
2011
- topProjectsByProvider: topProjectsByProvider,
2012
- modelBreakdownsByProvider: modelBreakdownsByProvider,
2013
- projectBreakdownsByProvider: projectBreakdownsByProvider,
2014
- spendForecastsByProvider: spendForecastsByProvider,
2015
- projectSpendForecastsByProvider: projectSpendForecastsByProvider,
2016
- topProjectSpendForecastsByProvider: topProjectSpendForecastsByProvider,
2017
- anomaliesByProvider: anomaliesByProvider,
2018
- errorsByProvider: errors,
2019
- lastActivityByProvider: lastActivityByProvider,
2020
- updatedAt: now,
2021
- providers: providers)
2060
+ activeBlocksByProvider: activeByProvider,
2061
+ topModelsByProvider: topModelsByProvider,
2062
+ topProjectsByProvider: topProjectsByProvider,
2063
+ modelBreakdownsByProvider: modelBreakdownsByProvider,
2064
+ projectBreakdownsByProvider: projectBreakdownsByProvider,
2065
+ spendForecastsByProvider: spendForecastsByProvider,
2066
+ projectSpendForecastsByProvider: projectSpendForecastsByProvider,
2067
+ topProjectSpendForecastsByProvider: topProjectSpendForecastsByProvider,
2068
+ anomaliesByProvider: anomaliesByProvider,
2069
+ errorsByProvider: errors,
2070
+ lastActivityByProvider: lastActivityByProvider,
2071
+ updatedAt: now,
2072
+ providers: providers)
2022
2073
  }
2023
2074
 
2024
2075
  private func projectNameOverridesFromBudgets() -> [String: String] {
@@ -2765,9 +2816,11 @@ extension UsageStore {
2765
2816
  ? nil : self.settings.azureOpenAIAPIToken,
2766
2817
  endpoint: self.settings.azureOpenAIEndpoint.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
2767
2818
  ? nil : self.settings.azureOpenAIEndpoint,
2768
- deployment: self.settings.azureOpenAIDeployment.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
2819
+ deployment: self.settings.azureOpenAIDeployment.trimmingCharacters(in: .whitespacesAndNewlines)
2820
+ .isEmpty
2769
2821
  ? nil : self.settings.azureOpenAIDeployment,
2770
- apiVersion: self.settings.azureOpenAIAPIVersion.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
2822
+ apiVersion: self.settings.azureOpenAIAPIVersion.trimmingCharacters(in: .whitespacesAndNewlines)
2823
+ .isEmpty
2771
2824
  ? nil : self.settings.azureOpenAIAPIVersion),
2772
2825
  bedrock: ProviderSettingsSnapshot.BedrockProviderSettings(
2773
2826
  region: self.settings.bedrockRegion.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
@@ -3099,8 +3152,8 @@ extension UsageStore {
3099
3152
  context: ProviderFetchContext,
3100
3153
  settings: ProviderSettingsSnapshot) async -> String
3101
3154
  {
3102
- var lines: [String] = [
3103
- "provider=\(provider.rawValue)"
3155
+ var lines = [
3156
+ "provider=\(provider.rawValue)",
3104
3157
  ]
3105
3158
 
3106
3159
  lines.append(contentsOf: self.debugCredentialLines(for: provider, settings: settings))
@@ -3145,38 +3198,54 @@ extension UsageStore {
3145
3198
  resolution: ProviderTokenResolver.minimaxGroupResolution()),
3146
3199
  ]
3147
3200
  case .openrouter:
3148
- return [self.debugTokenSummary(provider: "openrouter", resolution: ProviderTokenResolver.openRouterResolution())]
3201
+ return [self.debugTokenSummary(
3202
+ provider: "openrouter",
3203
+ resolution: ProviderTokenResolver.openRouterResolution())]
3149
3204
  case .groq:
3150
3205
  return [self.debugTokenSummary(provider: "groq", resolution: ProviderTokenResolver.groqResolution())]
3151
3206
  case .deepseek:
3152
- return [self.debugTokenSummary(provider: "deepseek", resolution: ProviderTokenResolver.deepSeekResolution())]
3207
+ return [self.debugTokenSummary(
3208
+ provider: "deepseek",
3209
+ resolution: ProviderTokenResolver.deepSeekResolution())]
3153
3210
  case .fireworks:
3154
- return [self.debugTokenSummary(provider: "fireworks", resolution: ProviderTokenResolver.fireworksResolution())]
3211
+ return [self.debugTokenSummary(
3212
+ provider: "fireworks",
3213
+ resolution: ProviderTokenResolver.fireworksResolution())]
3155
3214
  case .mistral:
3156
3215
  return [self.debugTokenSummary(provider: "mistral", resolution: ProviderTokenResolver.mistralResolution())]
3157
3216
  case .perplexity:
3158
- return [self.debugTokenSummary(provider: "perplexity", resolution: ProviderTokenResolver.perplexityResolution())]
3217
+ return [self.debugTokenSummary(
3218
+ provider: "perplexity",
3219
+ resolution: ProviderTokenResolver.perplexityResolution())]
3159
3220
  case .kimi:
3160
3221
  return [self.debugTokenSummary(provider: "kimi", resolution: ProviderTokenResolver.kimiResolution())]
3161
3222
  case .auggie:
3162
3223
  return [self.debugTokenSummary(provider: "auggie", resolution: ProviderTokenResolver.auggieResolution())]
3163
3224
  case .together:
3164
- return [self.debugTokenSummary(provider: "together", resolution: ProviderTokenResolver.togetherResolution())]
3225
+ return [self.debugTokenSummary(
3226
+ provider: "together",
3227
+ resolution: ProviderTokenResolver.togetherResolution())]
3165
3228
  case .cohere:
3166
3229
  return [self.debugTokenSummary(provider: "cohere", resolution: ProviderTokenResolver.cohereResolution())]
3167
3230
  case .xai:
3168
3231
  return [self.debugTokenSummary(provider: "xai", resolution: ProviderTokenResolver.xaiResolution())]
3169
3232
  case .cerebras:
3170
- return [self.debugTokenSummary(provider: "cerebras", resolution: ProviderTokenResolver.cerebrasResolution())]
3233
+ return [self.debugTokenSummary(
3234
+ provider: "cerebras",
3235
+ resolution: ProviderTokenResolver.cerebrasResolution())]
3171
3236
  case .sambanova:
3172
- return [self.debugTokenSummary(provider: "sambanova", resolution: ProviderTokenResolver.sambaNovaResolution())]
3237
+ return [self.debugTokenSummary(
3238
+ provider: "sambanova",
3239
+ resolution: ProviderTokenResolver.sambaNovaResolution())]
3173
3240
  case .azure:
3174
3241
  var lines: [String] = [
3175
3242
  "azure.endpoint=\(self.debugFieldValue(settings.azure?.endpoint))",
3176
3243
  "azure.deployment=\(self.debugFieldValue(settings.azure?.deployment))",
3177
- "azure.apiVersion=\(self.debugFieldValue(settings.azure?.apiVersion))"
3244
+ "azure.apiVersion=\(self.debugFieldValue(settings.azure?.apiVersion))",
3178
3245
  ]
3179
- lines.append(self.debugTokenSummary(provider: "azure.token", resolution: ProviderTokenResolver.azureOpenAIResolution()))
3246
+ lines.append(self.debugTokenSummary(
3247
+ provider: "azure.token",
3248
+ resolution: ProviderTokenResolver.azureOpenAIResolution()))
3180
3249
  return lines
3181
3250
  case .bedrock:
3182
3251
  return [
@@ -3236,7 +3305,7 @@ extension UsageStore {
3236
3305
  let resetsAt = providerCost.resetsAt?.description ?? "nil"
3237
3306
  lines.append(
3238
3307
  "provider_cost.used=\(providerCost.used) limit=\(providerCost.limit) " +
3239
- "currency=\(providerCost.currencyCode) period=\(providerCost.period ?? "nil") resetsAt=\(resetsAt)")
3308
+ "currency=\(providerCost.currencyCode) period=\(providerCost.period ?? "nil") resetsAt=\(resetsAt)")
3240
3309
  }
3241
3310
  return lines
3242
3311
  }
@@ -3485,8 +3554,8 @@ extension UsageStore {
3485
3554
  requestID: String,
3486
3555
  startTime: Date,
3487
3556
  endTime: Date,
3488
- success: Bool
3489
- ) async {
3557
+ success: Bool) async
3558
+ {
3490
3559
  guard let storage = self.performanceStorage else { return }
3491
3560
 
3492
3561
  let metric = LatencyMetric(
@@ -3498,8 +3567,7 @@ extension UsageStore {
3498
3567
  endTime: endTime,
3499
3568
  durationMs: Int(endTime.timeIntervalSince(startTime) * 1000),
3500
3569
  success: success,
3501
- createdAt: Date()
3502
- )
3570
+ createdAt: Date())
3503
3571
 
3504
3572
  try? await storage.save(latency: metric)
3505
3573
  }
@@ -3515,8 +3583,7 @@ extension UsageStore {
3515
3583
  errorType: errorType,
3516
3584
  errorMessage: error.localizedDescription,
3517
3585
  retryCount: 0,
3518
- timestamp: Date()
3519
- )
3586
+ timestamp: Date())
3520
3587
 
3521
3588
  try? await storage.save(error: errorEvent)
3522
3589
  }