@sriinnu/runic 1.2.0 → 2.11.1

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 (586) hide show
  1. package/CHANGELOG.md +267 -0
  2. package/LICENSE +373 -21
  3. package/README.md +98 -84
  4. package/THIRD-PARTY-NOTICES.md +651 -0
  5. package/package.json +14 -4
  6. package/.swiftformat +0 -50
  7. package/.swiftlint.yml +0 -160
  8. package/CONTRIBUTING.md +0 -456
  9. package/Icon.icns +0 -0
  10. package/Icon.icon/Assets/runic.png +0 -0
  11. package/Icon.icon/icon.json +0 -36
  12. package/Package.resolved +0 -51
  13. package/Package.swift +0 -136
  14. package/Packages/Helix/LICENSE +0 -21
  15. package/Packages/Helix/Package.swift +0 -29
  16. package/Packages/Helix/README.md +0 -976
  17. package/Packages/Helix/Sources/Helix/Binding.swift +0 -111
  18. package/Packages/Helix/Sources/Helix/CommandParser.swift +0 -160
  19. package/Packages/Helix/Sources/Helix/CommandSignature.swift +0 -117
  20. package/Packages/Helix/Sources/Helix/ExpressibleFromArgument.swift +0 -72
  21. package/Packages/Helix/Sources/Helix/HelpGenerator.swift +0 -175
  22. package/Packages/Helix/Sources/Helix/NameSpecification.swift +0 -98
  23. package/Packages/Helix/Sources/Helix/ParsableCommand.swift +0 -204
  24. package/Packages/Helix/Sources/Helix/Platform/DarwinPlatformContext.swift +0 -44
  25. package/Packages/Helix/Sources/Helix/Platform/LinuxPlatformContext.swift +0 -34
  26. package/Packages/Helix/Sources/Helix/Platform/PlatformContext+Default.swift +0 -91
  27. package/Packages/Helix/Sources/Helix/Platform/PlatformContext.swift +0 -94
  28. package/Packages/Helix/Sources/Helix/Platform/PlatformPath.swift +0 -167
  29. package/Packages/Helix/Sources/Helix/Platform/Stdio.swift +0 -99
  30. package/Packages/Helix/Sources/Helix/Platform/WebPlatformContext.swift +0 -146
  31. package/Packages/Helix/Sources/Helix/Platform/WindowsPlatformContext.swift +0 -97
  32. package/Packages/Helix/Sources/Helix/Program.swift +0 -204
  33. package/Packages/Helix/Sources/Helix/PropertyWrappers.swift +0 -367
  34. package/Packages/Silo/LICENSE +0 -21
  35. package/Packages/Silo/Package.swift +0 -34
  36. package/Packages/Silo/README.md +0 -466
  37. package/Packages/Silo/Sources/Silo/BinaryCookiesReader.swift +0 -169
  38. package/Packages/Silo/Sources/Silo/BrowserCookieAnalytics.swift +0 -365
  39. package/Packages/Silo/Sources/Silo/BrowserCookieBackends.swift +0 -594
  40. package/Packages/Silo/Sources/Silo/BrowserCookieClient.swift +0 -326
  41. package/Packages/Silo/Sources/Silo/BrowserCookieExport.swift +0 -154
  42. package/Packages/Silo/Sources/Silo/BrowserCookieImport.swift +0 -119
  43. package/Packages/Silo/Sources/Silo/BrowserCookieMockStore.swift +0 -121
  44. package/Packages/Silo/Sources/Silo/BrowserCookieModels.swift +0 -391
  45. package/Packages/Silo/Sources/Silo/ChromiumCookieReader.swift +0 -240
  46. package/Packages/Silo/Sources/Silo/CryptoSupport.swift +0 -86
  47. package/Packages/Silo/Sources/Silo/FirefoxCookieReader.swift +0 -172
  48. package/Packages/Silo/Sources/Silo/LinuxChromiumCookieReader.swift +0 -172
  49. package/Packages/Silo/Sources/Silo/LinuxSecretService.swift +0 -105
  50. package/Packages/Silo/Sources/Silo/MacOSChromiumCookieReader.swift +0 -191
  51. package/Packages/Silo/Sources/Silo/SQLiteSnapshot.swift +0 -41
  52. package/Packages/Silo/Sources/Silo/WindowsChromiumCookieReader.swift +0 -110
  53. package/RuniciOS/App/RuniciOSApp.swift +0 -105
  54. package/RuniciOS/Core/iOSUsageStore.swift +0 -192
  55. package/RuniciOS/Views/ProviderDetailView.swift +0 -312
  56. package/RuniciOS/Views/ProviderListView.swift +0 -228
  57. package/RuniciOS/Widgets/LargeWidget.swift +0 -367
  58. package/RuniciOS/Widgets/LiveActivity.swift +0 -342
  59. package/RuniciOS/Widgets/LockScreenWidget.swift +0 -273
  60. package/RuniciOS/Widgets/MediumWidget.swift +0 -370
  61. package/RuniciOS/Widgets/RunicWidget.swift +0 -274
  62. package/RuniciOS/Widgets/SmallWidget.swift +0 -281
  63. package/RuniciOS/Widgets/WidgetModels.swift +0 -285
  64. package/RuniciOS/Widgets/WidgetTimeline.swift +0 -270
  65. package/Scripts/CREATION-SUMMARY.md +0 -367
  66. package/Scripts/DIRECTORY-STRUCTURE.md +0 -358
  67. package/Scripts/GETTING-STARTED-CHECKLIST.md +0 -364
  68. package/Scripts/INDEX.md +0 -365
  69. package/Scripts/QUICK-REFERENCE.md +0 -164
  70. package/Scripts/README-SCRIPTS.md +0 -926
  71. package/Scripts/build-android.sh +0 -526
  72. package/Scripts/build-ios.sh +0 -529
  73. package/Scripts/build-macos.sh +0 -603
  74. package/Scripts/build-windows.sh +0 -504
  75. package/Scripts/build_icon.sh +0 -49
  76. package/Scripts/changelog-to-html.sh +0 -89
  77. package/Scripts/check-release-assets.sh +0 -10
  78. package/Scripts/ci-config.yml +0 -474
  79. package/Scripts/compile_and_run.sh +0 -219
  80. package/Scripts/compile_and_run_adhoc.sh +0 -4
  81. package/Scripts/docs-list.mjs +0 -119
  82. package/Scripts/make_appcast.sh +0 -58
  83. package/Scripts/package_app.sh +0 -409
  84. package/Scripts/release-lib.sh +0 -214
  85. package/Scripts/release.sh +0 -76
  86. package/Scripts/setup-all.sh +0 -387
  87. package/Scripts/setup-ios.sh +0 -404
  88. package/Scripts/setup-react-native.sh +0 -523
  89. package/Scripts/sign-and-notarize.sh +0 -122
  90. package/Scripts/test_live_update.sh +0 -38
  91. package/Scripts/validate_changelog.sh +0 -23
  92. package/Scripts/verify_appcast.sh +0 -92
  93. package/Sources/Runic/About.swift +0 -93
  94. package/Sources/Runic/AppNotifications.swift +0 -115
  95. package/Sources/Runic/Controllers/StatusItemController+Actions.swift +0 -666
  96. package/Sources/Runic/Controllers/StatusItemController+Animation.swift +0 -518
  97. package/Sources/Runic/Controllers/StatusItemController+ChartSubmenus.swift +0 -810
  98. package/Sources/Runic/Controllers/StatusItemController+CustomProviders.swift +0 -87
  99. package/Sources/Runic/Controllers/StatusItemController+Menu.swift +0 -1407
  100. package/Sources/Runic/Controllers/StatusItemController+MenuCards.swift +0 -337
  101. package/Sources/Runic/Controllers/StatusItemController+MenuHelpers.swift +0 -395
  102. package/Sources/Runic/Controllers/StatusItemController.swift +0 -360
  103. package/Sources/Runic/Core/BudgetNotificationManager.swift +0 -80
  104. package/Sources/Runic/Core/Login/ClaudeLoginRunner.swift +0 -103
  105. package/Sources/Runic/Core/Login/CodexLoginRunner.swift +0 -154
  106. package/Sources/Runic/Core/Login/CursorLoginRunner.swift +0 -206
  107. package/Sources/Runic/Core/Login/GeminiLoginRunner.swift +0 -92
  108. package/Sources/Runic/Core/Rendering/IconRenderer.swift +0 -1216
  109. package/Sources/Runic/Core/Rendering/IconView.swift +0 -120
  110. package/Sources/Runic/Core/Rendering/ProviderBrandIcon.swift +0 -71
  111. package/Sources/Runic/Core/RunicAnimation.swift +0 -53
  112. package/Sources/Runic/Core/RunicColors.swift +0 -73
  113. package/Sources/Runic/Core/RunicCornerRadius.swift +0 -15
  114. package/Sources/Runic/Core/RunicSpacing.swift +0 -68
  115. package/Sources/Runic/Core/Stores/AuggieTokenStore.swift +0 -119
  116. package/Sources/Runic/Core/Stores/AzureOpenAITokenStore.swift +0 -119
  117. package/Sources/Runic/Core/Stores/CerebrasTokenStore.swift +0 -119
  118. package/Sources/Runic/Core/Stores/CohereTokenStore.swift +0 -119
  119. package/Sources/Runic/Core/Stores/CopilotTokenStore.swift +0 -119
  120. package/Sources/Runic/Core/Stores/CustomProviderStore.swift +0 -158
  121. package/Sources/Runic/Core/Stores/DeepSeekTokenStore.swift +0 -119
  122. package/Sources/Runic/Core/Stores/FireworksTokenStore.swift +0 -119
  123. package/Sources/Runic/Core/Stores/GroqTokenStore.swift +0 -119
  124. package/Sources/Runic/Core/Stores/KimiTokenStore.swift +0 -119
  125. package/Sources/Runic/Core/Stores/MiniMaxCookieHeaderStore.swift +0 -119
  126. package/Sources/Runic/Core/Stores/MiniMaxGroupIDStore.swift +0 -119
  127. package/Sources/Runic/Core/Stores/MiniMaxTokenStore.swift +0 -119
  128. package/Sources/Runic/Core/Stores/MistralTokenStore.swift +0 -119
  129. package/Sources/Runic/Core/Stores/OpenRouterTokenStore.swift +0 -119
  130. package/Sources/Runic/Core/Stores/PerplexityTokenStore.swift +0 -119
  131. package/Sources/Runic/Core/Stores/ProviderToggleStore.swift +0 -30
  132. package/Sources/Runic/Core/Stores/QwenTokenStore.swift +0 -119
  133. package/Sources/Runic/Core/Stores/SambaNovaTokenStore.swift +0 -119
  134. package/Sources/Runic/Core/Stores/SettingsStore.swift +0 -1711
  135. package/Sources/Runic/Core/Stores/TogetherTokenStore.swift +0 -119
  136. package/Sources/Runic/Core/Stores/UsageStore+Accessors.swift +0 -20
  137. package/Sources/Runic/Core/Stores/UsageStore+Status.swift +0 -189
  138. package/Sources/Runic/Core/Stores/UsageStore+TokenCost.swift +0 -33
  139. package/Sources/Runic/Core/Stores/UsageStore+WidgetSnapshot.swift +0 -67
  140. package/Sources/Runic/Core/Stores/UsageStore.swift +0 -3605
  141. package/Sources/Runic/Core/Stores/XAITokenStore.swift +0 -119
  142. package/Sources/Runic/Core/Stores/ZaiTokenStore.swift +0 -118
  143. package/Sources/Runic/Core/UsageExporter.swift +0 -193
  144. package/Sources/Runic/Date+RelativeDescription.swift +0 -21
  145. package/Sources/Runic/HiddenWindowView.swift +0 -32
  146. package/Sources/Runic/InstallOrigin.swift +0 -9
  147. package/Sources/Runic/KeyboardShortcuts+Names.swift +0 -6
  148. package/Sources/Runic/MouseLocationReader.swift +0 -65
  149. package/Sources/Runic/Notifications+Runic.swift +0 -6
  150. package/Sources/Runic/OpenAICreditsPurchaseWindowController.swift +0 -503
  151. package/Sources/Runic/ProviderRegistry.swift +0 -100
  152. package/Sources/Runic/ProviderSwitcherButtons.swift +0 -267
  153. package/Sources/Runic/Providers/Antigravity/AntigravityLoginFlow.swift +0 -11
  154. package/Sources/Runic/Providers/Antigravity/AntigravityProviderImplementation.swift +0 -14
  155. package/Sources/Runic/Providers/Auggie/AuggieProviderImplementation.swift +0 -24
  156. package/Sources/Runic/Providers/Azure/AzureProviderImplementation.swift +0 -52
  157. package/Sources/Runic/Providers/Bedrock/BedrockProviderImplementation.swift +0 -42
  158. package/Sources/Runic/Providers/Cerebras/CerebrasProviderImplementation.swift +0 -24
  159. package/Sources/Runic/Providers/Claude/ClaudeLoginFlow.swift +0 -26
  160. package/Sources/Runic/Providers/Claude/ClaudeProviderImplementation.swift +0 -14
  161. package/Sources/Runic/Providers/Codex/CodexLoginFlow.swift +0 -18
  162. package/Sources/Runic/Providers/Codex/CodexProviderImplementation.swift +0 -15
  163. package/Sources/Runic/Providers/Cohere/CohereProviderImplementation.swift +0 -24
  164. package/Sources/Runic/Providers/Copilot/CopilotLoginFlow.swift +0 -193
  165. package/Sources/Runic/Providers/Copilot/CopilotProviderImplementation.swift +0 -110
  166. package/Sources/Runic/Providers/Cursor/CursorLoginFlow.swift +0 -28
  167. package/Sources/Runic/Providers/Cursor/CursorProviderImplementation.swift +0 -15
  168. package/Sources/Runic/Providers/DeepSeek/DeepSeekProviderImplementation.swift +0 -24
  169. package/Sources/Runic/Providers/Factory/FactoryLoginFlow.swift +0 -12
  170. package/Sources/Runic/Providers/Factory/FactoryProviderImplementation.swift +0 -15
  171. package/Sources/Runic/Providers/Fireworks/FireworksProviderImplementation.swift +0 -24
  172. package/Sources/Runic/Providers/Gemini/GeminiLoginFlow.swift +0 -20
  173. package/Sources/Runic/Providers/Gemini/GeminiProviderImplementation.swift +0 -15
  174. package/Sources/Runic/Providers/Groq/GroqProviderImplementation.swift +0 -24
  175. package/Sources/Runic/Providers/Kimi/KimiProviderImplementation.swift +0 -24
  176. package/Sources/Runic/Providers/MiniMax/MiniMaxProviderImplementation.swift +0 -32
  177. package/Sources/Runic/Providers/Mistral/MistralProviderImplementation.swift +0 -24
  178. package/Sources/Runic/Providers/OpenRouter/OpenRouterProviderImplementation.swift +0 -24
  179. package/Sources/Runic/Providers/Perplexity/PerplexityProviderImplementation.swift +0 -24
  180. package/Sources/Runic/Providers/SambaNova/SambaNovaProviderImplementation.swift +0 -24
  181. package/Sources/Runic/Providers/Shared/ProviderCatalog.swift +0 -15
  182. package/Sources/Runic/Providers/Shared/ProviderImplementation.swift +0 -48
  183. package/Sources/Runic/Providers/Shared/ProviderImplementationRegistry.swift +0 -66
  184. package/Sources/Runic/Providers/Shared/ProviderLoginFlow.swift +0 -12
  185. package/Sources/Runic/Providers/Shared/ProviderSettingsDescriptors.swift +0 -99
  186. package/Sources/Runic/Providers/Shared/SystemSettingsLinks.swift +0 -18
  187. package/Sources/Runic/Providers/Together/TogetherProviderImplementation.swift +0 -24
  188. package/Sources/Runic/Providers/XAI/XAIProviderImplementation.swift +0 -24
  189. package/Sources/Runic/Providers/Zai/ZaiProviderImplementation.swift +0 -24
  190. package/Sources/Runic/Resources/ProviderIcon-antigravity.svg +0 -2
  191. package/Sources/Runic/Resources/ProviderIcon-auggie.svg +0 -3
  192. package/Sources/Runic/Resources/ProviderIcon-azure.svg +0 -5
  193. package/Sources/Runic/Resources/ProviderIcon-bedrock.svg +0 -7
  194. package/Sources/Runic/Resources/ProviderIcon-cerebras.svg +0 -3
  195. package/Sources/Runic/Resources/ProviderIcon-claude.svg +0 -1
  196. package/Sources/Runic/Resources/ProviderIcon-codex.svg +0 -1
  197. package/Sources/Runic/Resources/ProviderIcon-cohere.svg +0 -3
  198. package/Sources/Runic/Resources/ProviderIcon-copilot.svg +0 -1
  199. package/Sources/Runic/Resources/ProviderIcon-cursor.svg +0 -3
  200. package/Sources/Runic/Resources/ProviderIcon-deepseek.svg +0 -4
  201. package/Sources/Runic/Resources/ProviderIcon-factory.svg +0 -3
  202. package/Sources/Runic/Resources/ProviderIcon-fireworks.svg +0 -3
  203. package/Sources/Runic/Resources/ProviderIcon-gemini.svg +0 -1
  204. package/Sources/Runic/Resources/ProviderIcon-groq.svg +0 -1
  205. package/Sources/Runic/Resources/ProviderIcon-kimi.svg +0 -4
  206. package/Sources/Runic/Resources/ProviderIcon-minimax.svg +0 -1
  207. package/Sources/Runic/Resources/ProviderIcon-mistral.svg +0 -5
  208. package/Sources/Runic/Resources/ProviderIcon-openrouter.svg +0 -7
  209. package/Sources/Runic/Resources/ProviderIcon-perplexity.svg +0 -4
  210. package/Sources/Runic/Resources/ProviderIcon-qwen.svg +0 -9
  211. package/Sources/Runic/Resources/ProviderIcon-sambanova.svg +0 -4
  212. package/Sources/Runic/Resources/ProviderIcon-together.svg +0 -5
  213. package/Sources/Runic/Resources/ProviderIcon-vertexai.svg +0 -17
  214. package/Sources/Runic/Resources/ProviderIcon-xai.svg +0 -3
  215. package/Sources/Runic/Resources/ProviderIcon-zai.svg +0 -5
  216. package/Sources/Runic/Resources/RunicMenubarIcon.svg +0 -7
  217. package/Sources/Runic/Resources/provider-context-windows.json +0 -20
  218. package/Sources/Runic/RunicApp.swift +0 -357
  219. package/Sources/Runic/SessionQuotaNotifications.swift +0 -58
  220. package/Sources/Runic/UsagePaceText.swift +0 -52
  221. package/Sources/Runic/UsageProgressBar.swift +0 -224
  222. package/Sources/Runic/Utilities/DisplayLink.swift +0 -96
  223. package/Sources/Runic/Utilities/LoadingPattern.swift +0 -60
  224. package/Sources/Runic/Utilities/PerformanceConstants.swift +0 -85
  225. package/Sources/Runic/Views/Menu/AlertsMenuView.swift +0 -256
  226. package/Sources/Runic/Views/Menu/ChartPanelStyle.swift +0 -22
  227. package/Sources/Runic/Views/Menu/CostComparativeChartMenuView.swift +0 -275
  228. package/Sources/Runic/Views/Menu/CostHistoryChartMenuView.swift +0 -377
  229. package/Sources/Runic/Views/Menu/CreditsHistoryChartMenuView.swift +0 -312
  230. package/Sources/Runic/Views/Menu/EfficiencyMetricsMenuView.swift +0 -312
  231. package/Sources/Runic/Views/Menu/HeroStatCardsMenuView.swift +0 -228
  232. package/Sources/Runic/Views/Menu/HourlyActivityChartMenuView.swift +0 -228
  233. package/Sources/Runic/Views/Menu/InlineUsageChartView.swift +0 -155
  234. package/Sources/Runic/Views/Menu/MenuCardEntranceModifier.swift +0 -64
  235. package/Sources/Runic/Views/Menu/MenuCardSidebarView.swift +0 -268
  236. package/Sources/Runic/Views/Menu/MenuCardSkeletonView.swift +0 -104
  237. package/Sources/Runic/Views/Menu/MenuCardView.swift +0 -1900
  238. package/Sources/Runic/Views/Menu/MenuContent.swift +0 -150
  239. package/Sources/Runic/Views/Menu/MenuDescriptor.swift +0 -369
  240. package/Sources/Runic/Views/Menu/MenuHighlightStyle.swift +0 -42
  241. package/Sources/Runic/Views/Menu/ModelBreakdownMenuView.swift +0 -235
  242. package/Sources/Runic/Views/Menu/OverviewMenuView.swift +0 -278
  243. package/Sources/Runic/Views/Menu/ProjectBreakdownMenuView.swift +0 -209
  244. package/Sources/Runic/Views/Menu/ProjectBudgetMenuView.swift +0 -220
  245. package/Sources/Runic/Views/Menu/ProviderTabBarView.swift +0 -88
  246. package/Sources/Runic/Views/Menu/SubscriptionUtilizationChartMenuView.swift +0 -201
  247. package/Sources/Runic/Views/Menu/UsageBreakdownChartMenuView.swift +0 -398
  248. package/Sources/Runic/Views/Menu/UsageHeatmapMenuView.swift +0 -232
  249. package/Sources/Runic/Views/Menu/UsageTimelineChartMenuView.swift +0 -364
  250. package/Sources/Runic/Views/Menu/UsageWindowComparisonChartMenuView.swift +0 -190
  251. package/Sources/Runic/Views/Menu/WeeklyActivityChartMenuView.swift +0 -132
  252. package/Sources/Runic/Views/Performance/PerformanceDashboardView.swift +0 -618
  253. package/Sources/Runic/Views/Preferences/LiquidComponents.swift +0 -305
  254. package/Sources/Runic/Views/Preferences/PreferencesAboutPane.swift +0 -271
  255. package/Sources/Runic/Views/Preferences/PreferencesAdvancedPane.swift +0 -268
  256. package/Sources/Runic/Views/Preferences/PreferencesAlertsPane.swift +0 -422
  257. package/Sources/Runic/Views/Preferences/PreferencesAnalyticsPane.swift +0 -641
  258. package/Sources/Runic/Views/Preferences/PreferencesBudgetsPane.swift +0 -344
  259. package/Sources/Runic/Views/Preferences/PreferencesComponents.swift +0 -230
  260. package/Sources/Runic/Views/Preferences/PreferencesCustomProvidersPane.swift +0 -322
  261. package/Sources/Runic/Views/Preferences/PreferencesDebugPane.swift +0 -469
  262. package/Sources/Runic/Views/Preferences/PreferencesGeneralPane.swift +0 -231
  263. package/Sources/Runic/Views/Preferences/PreferencesHelpPane.swift +0 -50
  264. package/Sources/Runic/Views/Preferences/PreferencesIntegrationsPane.swift +0 -567
  265. package/Sources/Runic/Views/Preferences/PreferencesPerformancePane.swift +0 -288
  266. package/Sources/Runic/Views/Preferences/PreferencesProvidersPane.swift +0 -3435
  267. package/Sources/Runic/Views/Preferences/PreferencesSelection.swift +0 -25
  268. package/Sources/Runic/Views/Preferences/PreferencesSyncPane.swift +0 -59
  269. package/Sources/Runic/Views/Preferences/PreferencesView.swift +0 -206
  270. package/Sources/Runic/Views/Rating/QualityRatingView.swift +0 -238
  271. package/Sources/Runic/Views/Team/TeamDashboardView.swift +0 -371
  272. package/Sources/Runic/Views/Team/TeamInviteSheet.swift +0 -175
  273. package/Sources/Runic/Views/Team/TeamManagementView.swift +0 -761
  274. package/Sources/Runic/Views/Team/TeamMemberRow.swift +0 -181
  275. package/Sources/RunicCLI/CLIEntry.swift +0 -942
  276. package/Sources/RunicCLI/CLIRenderer.swift +0 -87
  277. package/Sources/RunicCLI/Commands/EnhancedUsageCommand.swift +0 -588
  278. package/Sources/RunicCLI/Commands/ModelsCommand.swift +0 -362
  279. package/Sources/RunicCLI/Commands/ProjectsCommand.swift +0 -439
  280. package/Sources/RunicCLI/Commands/ResetCommand.swift +0 -429
  281. package/Sources/RunicCLI/Commands/UsageLedgerSourceFactory.swift +0 -168
  282. package/Sources/RunicClaudeWatchdog/main.swift +0 -122
  283. package/Sources/RunicClaudeWebProbe/main.swift +0 -62
  284. package/Sources/RunicCore/AlertRuleStore.swift +0 -187
  285. package/Sources/RunicCore/BrowserCookieImportOrder.swift +0 -25
  286. package/Sources/RunicCore/CopilotUsageModels.swift +0 -84
  287. package/Sources/RunicCore/CostUsageFetcher.swift +0 -119
  288. package/Sources/RunicCore/CostUsageModels.swift +0 -479
  289. package/Sources/RunicCore/CreditsModels.swift +0 -50
  290. package/Sources/RunicCore/CustomProviders/CustomProviderModels.swift +0 -269
  291. package/Sources/RunicCore/CustomProviders/GenericProviderFetcher.swift +0 -633
  292. package/Sources/RunicCore/CustomProviders/ProviderTemplates.swift +0 -500
  293. package/Sources/RunicCore/Host/PTY/TTYCommandRunner.swift +0 -679
  294. package/Sources/RunicCore/Host/Process/SubprocessRunner.swift +0 -135
  295. package/Sources/RunicCore/Integrations/GitHubIntegration.swift +0 -173
  296. package/Sources/RunicCore/Logging/JSONStderrLogHandler.swift +0 -79
  297. package/Sources/RunicCore/Logging/OSLogLogHandler.swift +0 -86
  298. package/Sources/RunicCore/Logging/RunicLog.swift +0 -123
  299. package/Sources/RunicCore/Models/EnhancedUsageModels.swift +0 -371
  300. package/Sources/RunicCore/OpenAIDashboardModels.swift +0 -210
  301. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardBrowserCookieImporter.swift +0 -676
  302. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardFetcher.swift +0 -492
  303. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardNavigationDelegate.swift +0 -51
  304. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardParser.swift +0 -483
  305. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardScrapeScript.swift +0 -558
  306. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardWebViewCache.swift +0 -204
  307. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardWebsiteDataStore.swift +0 -78
  308. package/Sources/RunicCore/PathEnvironment.swift +0 -482
  309. package/Sources/RunicCore/Performance/PerformanceModels.swift +0 -371
  310. package/Sources/RunicCore/Performance/PerformanceStorageImpl.swift +0 -409
  311. package/Sources/RunicCore/Policy/UsagePolicyEngine.swift +0 -178
  312. package/Sources/RunicCore/ProjectBudgetStore.swift +0 -99
  313. package/Sources/RunicCore/ProviderCostSnapshot.swift +0 -29
  314. package/Sources/RunicCore/Providers/Antigravity/AntigravityProviderDescriptor.swift +0 -66
  315. package/Sources/RunicCore/Providers/Antigravity/AntigravityStatusProbe.swift +0 -685
  316. package/Sources/RunicCore/Providers/Auggie/AuggieProviderDescriptor.swift +0 -83
  317. package/Sources/RunicCore/Providers/Auggie/AuggieUsageFetcher.swift +0 -142
  318. package/Sources/RunicCore/Providers/Azure/AzureProviderDescriptor.swift +0 -141
  319. package/Sources/RunicCore/Providers/Azure/AzureUsageFetcher.swift +0 -188
  320. package/Sources/RunicCore/Providers/Bedrock/BedrockProviderDescriptor.swift +0 -126
  321. package/Sources/RunicCore/Providers/Bedrock/BedrockUsageFetcher.swift +0 -168
  322. package/Sources/RunicCore/Providers/Cerebras/CerebrasProviderDescriptor.swift +0 -84
  323. package/Sources/RunicCore/Providers/Cerebras/CerebrasUsageFetcher.swift +0 -75
  324. package/Sources/RunicCore/Providers/Claude/ClaudeCLISession.swift +0 -332
  325. package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift +0 -180
  326. package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthUsageFetcher.swift +0 -137
  327. package/Sources/RunicCore/Providers/Claude/ClaudeProviderDescriptor.swift +0 -207
  328. package/Sources/RunicCore/Providers/Claude/ClaudeStatusProbe.swift +0 -654
  329. package/Sources/RunicCore/Providers/Claude/ClaudeUsageDataSource.swift +0 -28
  330. package/Sources/RunicCore/Providers/Claude/ClaudeUsageFetcher.swift +0 -480
  331. package/Sources/RunicCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift +0 -780
  332. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthCredentials.swift +0 -147
  333. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift +0 -280
  334. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexTokenRefresher.swift +0 -100
  335. package/Sources/RunicCore/Providers/Codex/CodexProviderDescriptor.swift +0 -243
  336. package/Sources/RunicCore/Providers/Codex/CodexStatusProbe.swift +0 -125
  337. package/Sources/RunicCore/Providers/Codex/CodexUsageDataSource.swift +0 -24
  338. package/Sources/RunicCore/Providers/Codex/CodexWebDashboardStrategy.swift +0 -181
  339. package/Sources/RunicCore/Providers/Cohere/CohereProviderDescriptor.swift +0 -83
  340. package/Sources/RunicCore/Providers/Cohere/CohereUsageFetcher.swift +0 -85
  341. package/Sources/RunicCore/Providers/Copilot/CopilotDeviceFlow.swift +0 -123
  342. package/Sources/RunicCore/Providers/Copilot/CopilotGitHubCLITokenReader.swift +0 -114
  343. package/Sources/RunicCore/Providers/Copilot/CopilotProviderDescriptor.swift +0 -100
  344. package/Sources/RunicCore/Providers/Copilot/CopilotUsageFetcher.swift +0 -291
  345. package/Sources/RunicCore/Providers/Copilot/CopilotVSCodeTokenReader.swift +0 -386
  346. package/Sources/RunicCore/Providers/Cursor/CursorProviderDescriptor.swift +0 -65
  347. package/Sources/RunicCore/Providers/Cursor/CursorStatusProbe.swift +0 -595
  348. package/Sources/RunicCore/Providers/DeepSeek/DeepSeekProviderDescriptor.swift +0 -85
  349. package/Sources/RunicCore/Providers/DeepSeek/DeepSeekUsageFetcher.swift +0 -108
  350. package/Sources/RunicCore/Providers/Factory/FactoryLocalStorageImporter.swift +0 -398
  351. package/Sources/RunicCore/Providers/Factory/FactoryProviderDescriptor.swift +0 -65
  352. package/Sources/RunicCore/Providers/Factory/FactoryStatusProbe.swift +0 -1276
  353. package/Sources/RunicCore/Providers/Fireworks/FireworksProviderDescriptor.swift +0 -84
  354. package/Sources/RunicCore/Providers/Fireworks/FireworksUsageFetcher.swift +0 -75
  355. package/Sources/RunicCore/Providers/Gemini/GeminiProviderDescriptor.swift +0 -65
  356. package/Sources/RunicCore/Providers/Gemini/GeminiStatusProbe.swift +0 -836
  357. package/Sources/RunicCore/Providers/Groq/GroqProviderDescriptor.swift +0 -83
  358. package/Sources/RunicCore/Providers/Groq/GroqUsageFetcher.swift +0 -71
  359. package/Sources/RunicCore/Providers/Kimi/KimiProviderDescriptor.swift +0 -83
  360. package/Sources/RunicCore/Providers/Kimi/KimiUsageFetcher.swift +0 -77
  361. package/Sources/RunicCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift +0 -92
  362. package/Sources/RunicCore/Providers/MiniMax/MiniMaxSettingsReader.swift +0 -54
  363. package/Sources/RunicCore/Providers/MiniMax/MiniMaxUsageFetcher.swift +0 -168
  364. package/Sources/RunicCore/Providers/MiniMax/MiniMaxWebUsageFetcher.swift +0 -911
  365. package/Sources/RunicCore/Providers/Mistral/MistralProviderDescriptor.swift +0 -84
  366. package/Sources/RunicCore/Providers/Mistral/MistralUsageFetcher.swift +0 -75
  367. package/Sources/RunicCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift +0 -87
  368. package/Sources/RunicCore/Providers/OpenRouter/OpenRouterUsageFetcher.swift +0 -203
  369. package/Sources/RunicCore/Providers/Perplexity/PerplexityProviderDescriptor.swift +0 -84
  370. package/Sources/RunicCore/Providers/Perplexity/PerplexityUsageFetcher.swift +0 -75
  371. package/Sources/RunicCore/Providers/ProviderBranding.swift +0 -25
  372. package/Sources/RunicCore/Providers/ProviderCLIConfig.swift +0 -17
  373. package/Sources/RunicCore/Providers/ProviderDescriptor.swift +0 -129
  374. package/Sources/RunicCore/Providers/ProviderFetchPlan.swift +0 -263
  375. package/Sources/RunicCore/Providers/ProviderSettingsSnapshot.swift +0 -98
  376. package/Sources/RunicCore/Providers/ProviderTokenResolver.swift +0 -501
  377. package/Sources/RunicCore/Providers/ProviderVersionDetector.swift +0 -68
  378. package/Sources/RunicCore/Providers/Providers.swift +0 -184
  379. package/Sources/RunicCore/Providers/Qwen/QwenProviderDescriptor.swift +0 -84
  380. package/Sources/RunicCore/Providers/Qwen/QwenSettingsReader.swift +0 -30
  381. package/Sources/RunicCore/Providers/Qwen/QwenUsageStats.swift +0 -315
  382. package/Sources/RunicCore/Providers/SambaNova/SambaNovaProviderDescriptor.swift +0 -84
  383. package/Sources/RunicCore/Providers/SambaNova/SambaNovaUsageFetcher.swift +0 -75
  384. package/Sources/RunicCore/Providers/Together/TogetherProviderDescriptor.swift +0 -83
  385. package/Sources/RunicCore/Providers/Together/TogetherUsageFetcher.swift +0 -75
  386. package/Sources/RunicCore/Providers/VertexAI/VertexAIProviderDescriptor.swift +0 -123
  387. package/Sources/RunicCore/Providers/VertexAI/VertexAISettingsReader.swift +0 -48
  388. package/Sources/RunicCore/Providers/VertexAI/VertexAIUsageFetcher.swift +0 -144
  389. package/Sources/RunicCore/Providers/XAI/XAIProviderDescriptor.swift +0 -83
  390. package/Sources/RunicCore/Providers/XAI/XAIUsageFetcher.swift +0 -75
  391. package/Sources/RunicCore/Providers/Zai/ZaiProviderDescriptor.swift +0 -72
  392. package/Sources/RunicCore/Providers/Zai/ZaiSettingsReader.swift +0 -41
  393. package/Sources/RunicCore/Providers/Zai/ZaiUsageStats.swift +0 -718
  394. package/Sources/RunicCore/Sync/BackgroundSyncManager.swift +0 -385
  395. package/Sources/RunicCore/Sync/SyncConflictResolver.swift +0 -310
  396. package/Sources/RunicCore/Sync/SyncProtocol.swift +0 -288
  397. package/Sources/RunicCore/Sync/SyncRecord.swift +0 -405
  398. package/Sources/RunicCore/Sync/SyncUsageExample.swift +0 -294
  399. package/Sources/RunicCore/Sync/TeamSyncRecords.swift +0 -775
  400. package/Sources/RunicCore/Sync/iCloudSyncEngine.swift +0 -333
  401. package/Sources/RunicCore/Team/TeamModels.swift +0 -583
  402. package/Sources/RunicCore/Team/TeamShowbackExporter.swift +0 -495
  403. package/Sources/RunicCore/Team/TeamStore.swift +0 -792
  404. package/Sources/RunicCore/TextParsing.swift +0 -55
  405. package/Sources/RunicCore/UsageFetcher.swift +0 -694
  406. package/Sources/RunicCore/UsageFormatter.swift +0 -410
  407. package/Sources/RunicCore/UsageLedger/ClaudeUsageLogSource.swift +0 -427
  408. package/Sources/RunicCore/UsageLedger/CodexUsageLogSource.swift +0 -494
  409. package/Sources/RunicCore/UsageLedger/LedgerCache.swift +0 -158
  410. package/Sources/RunicCore/UsageLedger/OTelGenAILedgerAdapter.swift +0 -550
  411. package/Sources/RunicCore/UsageLedger/UsageLedger.swift +0 -29
  412. package/Sources/RunicCore/UsageLedger/UsageLedgerAggregator.swift +0 -660
  413. package/Sources/RunicCore/UsageLedger/UsageLedgerInsightsAdvisor.swift +0 -209
  414. package/Sources/RunicCore/UsageLedger/UsageLedgerModels.swift +0 -417
  415. package/Sources/RunicCore/UsageLedger/UsageLedgerProjectIdentity.swift +0 -245
  416. package/Sources/RunicCore/UsagePace.swift +0 -81
  417. package/Sources/RunicCore/Vendored/CostUsage/CostUsageCache.swift +0 -73
  418. package/Sources/RunicCore/Vendored/CostUsage/CostUsageJsonl.swift +0 -74
  419. package/Sources/RunicCore/Vendored/CostUsage/CostUsagePricing.swift +0 -208
  420. package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner+Claude.swift +0 -455
  421. package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner+Timestamp.swift +0 -132
  422. package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner.swift +0 -589
  423. package/Sources/RunicCore/WidgetSnapshot.swift +0 -183
  424. package/Sources/RunicMacroSupport/ProviderRegistrationMacros.swift +0 -14
  425. package/Sources/RunicMacros/ProviderRegistrationMacros.swift +0 -216
  426. package/Sources/RunicWidget/RunicWidgetBundle.swift +0 -79
  427. package/Sources/RunicWidget/RunicWidgetProvider.swift +0 -283
  428. package/Sources/RunicWidget/RunicWidgetViews.swift +0 -680
  429. package/Tests/RunicTests/AntigravityStatusProbeTests.swift +0 -50
  430. package/Tests/RunicTests/AppDelegateTests.swift +0 -43
  431. package/Tests/RunicTests/BrowserCookieOrderLabelTests.swift +0 -22
  432. package/Tests/RunicTests/CLIArgumentParsingTests.swift +0 -70
  433. package/Tests/RunicTests/CLICostTests.swift +0 -65
  434. package/Tests/RunicTests/CLIProviderSelectionTests.swift +0 -22
  435. package/Tests/RunicTests/CLISnapshotTests.swift +0 -72
  436. package/Tests/RunicTests/CLIWebFallbackTests.swift +0 -58
  437. package/Tests/RunicTests/ClaudeOAuthTests.swift +0 -164
  438. package/Tests/RunicTests/ClaudeResilienceTests.swift +0 -30
  439. package/Tests/RunicTests/ClaudeUsageTests.swift +0 -370
  440. package/Tests/RunicTests/CodexOAuthTests.swift +0 -122
  441. package/Tests/RunicTests/CodexUsageLogSourceTests.swift +0 -82
  442. package/Tests/RunicTests/CopilotTokenResolverTests.swift +0 -75
  443. package/Tests/RunicTests/CopilotVSCodeTokenReaderTests.swift +0 -75
  444. package/Tests/RunicTests/CostUsageDecodingTests.swift +0 -394
  445. package/Tests/RunicTests/CostUsagePricingTests.swift +0 -50
  446. package/Tests/RunicTests/CostUsageScannerTests.swift +0 -700
  447. package/Tests/RunicTests/CursorStatusProbeTests.swift +0 -323
  448. package/Tests/RunicTests/FactoryStatusProbeTests.swift +0 -69
  449. package/Tests/RunicTests/GeminiAPITestHelpers.swift +0 -94
  450. package/Tests/RunicTests/GeminiLoginAlertTests.swift +0 -28
  451. package/Tests/RunicTests/GeminiStatusProbeAPITests.swift +0 -283
  452. package/Tests/RunicTests/GeminiStatusProbePlanTests.swift +0 -279
  453. package/Tests/RunicTests/GeminiStatusProbeTests.swift +0 -233
  454. package/Tests/RunicTests/GeminiTestEnvironment.swift +0 -87
  455. package/Tests/RunicTests/GoogleWorkspaceStatusTests.swift +0 -73
  456. package/Tests/RunicTests/InstallOriginTests.swift +0 -18
  457. package/Tests/RunicTests/KeyboardShortcutsBundleTests.swift +0 -9
  458. package/Tests/RunicTests/LiveAccountTests.swift +0 -24
  459. package/Tests/RunicTests/LoadingPatternTests.swift +0 -35
  460. package/Tests/RunicTests/MenuCardModelTests.swift +0 -545
  461. package/Tests/RunicTests/MiniMaxCookieHeaderStoreTestSupport.swift +0 -8
  462. package/Tests/RunicTests/MiniMaxGroupIDStoreTestSupport.swift +0 -8
  463. package/Tests/RunicTests/MiniMaxParsingTests.swift +0 -68
  464. package/Tests/RunicTests/MiniMaxTokenStoreTestSupport.swift +0 -8
  465. package/Tests/RunicTests/OTelGenAILedgerAdapterTests.swift +0 -112
  466. package/Tests/RunicTests/OpenAIDashboardBrowserCookieImporterTests.swift +0 -17
  467. package/Tests/RunicTests/OpenAIDashboardFetcherCreditsWaitTests.swift +0 -75
  468. package/Tests/RunicTests/OpenAIDashboardOffscreenHostTests.swift +0 -27
  469. package/Tests/RunicTests/OpenAIDashboardParserTests.swift +0 -134
  470. package/Tests/RunicTests/OpenAIWebAccountSwitchTests.swift +0 -53
  471. package/Tests/RunicTests/OpenRouterTokenStoreTestSupport.swift +0 -92
  472. package/Tests/RunicTests/PathBuilderTests.swift +0 -237
  473. package/Tests/RunicTests/ProviderFetchPipelineTests.swift +0 -160
  474. package/Tests/RunicTests/ProviderIconResourcesTests.swift +0 -38
  475. package/Tests/RunicTests/ProviderMetadataStatusLinkTests.swift +0 -16
  476. package/Tests/RunicTests/ProviderRegistryTests.swift +0 -29
  477. package/Tests/RunicTests/ProviderSettingsDescriptorTests.swift +0 -202
  478. package/Tests/RunicTests/ProviderToggleStoreTests.swift +0 -51
  479. package/Tests/RunicTests/RunicTests.swift +0 -120
  480. package/Tests/RunicTests/SessionQuotaNotificationLogicTests.swift +0 -36
  481. package/Tests/RunicTests/SettingsStoreTests.swift +0 -161
  482. package/Tests/RunicTests/StatusItemAnimationTests.swift +0 -49
  483. package/Tests/RunicTests/StatusMenuTests.swift +0 -434
  484. package/Tests/RunicTests/StatusProbeTests.swift +0 -377
  485. package/Tests/RunicTests/SubprocessRunnerTests.swift +0 -20
  486. package/Tests/RunicTests/SubscriptionDetectionTests.swift +0 -65
  487. package/Tests/RunicTests/TTYCommandRunnerTests.swift +0 -148
  488. package/Tests/RunicTests/TTYIntegrationTests.swift +0 -41
  489. package/Tests/RunicTests/TeamShowbackExporterTests.swift +0 -159
  490. package/Tests/RunicTests/TextParsingTests.swift +0 -12
  491. package/Tests/RunicTests/UsageFormatterTests.swift +0 -112
  492. package/Tests/RunicTests/UsageLedgerAnomalyDetectionTests.swift +0 -172
  493. package/Tests/RunicTests/UsageLedgerForecastTests.swift +0 -213
  494. package/Tests/RunicTests/UsageLedgerInsightsAdvisorTests.swift +0 -83
  495. package/Tests/RunicTests/UsageLedgerProjectNameTests.swift +0 -170
  496. package/Tests/RunicTests/UsagePaceTests.swift +0 -77
  497. package/Tests/RunicTests/UsagePaceTextTests.swift +0 -109
  498. package/Tests/RunicTests/UsagePolicyEngineTests.swift +0 -91
  499. package/Tests/RunicTests/WidgetSnapshotTests.swift +0 -45
  500. package/Tests/RunicTests/ZaiProviderTests.swift +0 -61
  501. package/Tests/RunicTests/ZaiTokenStoreTestSupport.swift +0 -6
  502. package/TestsLinux/PlatformGatingTests.swift +0 -46
  503. package/appcast.xml +0 -144
  504. package/assets/logo-concept-stack.svg +0 -24
  505. package/assets/logo-minimal-bars.svg +0 -15
  506. package/assets/logo.svg +0 -20
  507. package/assets/logo2.svg +0 -43
  508. package/bin/docs-list +0 -5
  509. package/build-and-run.sh +0 -369
  510. package/docs/.nojekyll +0 -1
  511. package/docs/CNAME +0 -1
  512. package/docs/antigravity.md +0 -75
  513. package/docs/architecture.md +0 -32
  514. package/docs/claude.md +0 -93
  515. package/docs/cli-commands/integration_guide.md +0 -371
  516. package/docs/cli-commands/quick_reference.md +0 -362
  517. package/docs/cli-commands/readme.md +0 -361
  518. package/docs/cli.md +0 -156
  519. package/docs/cloudkit_sync_guide.md +0 -495
  520. package/docs/codebase_review.md +0 -288
  521. package/docs/codex-oauth.md +0 -635
  522. package/docs/codex.md +0 -104
  523. package/docs/codexbar.png +0 -0
  524. package/docs/copilot.md +0 -44
  525. package/docs/cursor.md +0 -46
  526. package/docs/design/designcomparisonchart.md +0 -510
  527. package/docs/design/implementationguide.md +0 -792
  528. package/docs/design/menubaricondesignconcepts.md +0 -796
  529. package/docs/design/mockups/hybridiconconcept.svg +0 -62
  530. package/docs/design/mockups/infinityiconconcept.svg +0 -41
  531. package/docs/design/mockups/infinityiconstates.svg +0 -129
  532. package/docs/design/mockups/wavelogorefined.svg +0 -48
  533. package/docs/design/quick_summary.md +0 -426
  534. package/docs/design/readme.md +0 -478
  535. package/docs/design/sizespecifications.md +0 -575
  536. package/docs/design-system.md +0 -836
  537. package/docs/factory.md +0 -103
  538. package/docs/gemini.md +0 -73
  539. package/docs/icon.md +0 -37
  540. package/docs/icon.png +0 -0
  541. package/docs/index.html +0 -175
  542. package/docs/keychain_fix.md +0 -274
  543. package/docs/minimax.md +0 -71
  544. package/docs/packaging.md +0 -25
  545. package/docs/progress-bar-improvements.md +0 -240
  546. package/docs/progress-bar-quick-reference.md +0 -283
  547. package/docs/progress-bar-technical-details.md +0 -432
  548. package/docs/progress-bar-visual-test.swift +0 -375
  549. package/docs/provider.md +0 -202
  550. package/docs/providers.md +0 -86
  551. package/docs/refactor/macros.md +0 -35
  552. package/docs/refresh-loop.md +0 -22
  553. package/docs/releasing-homebrew.md +0 -35
  554. package/docs/releasing.md +0 -121
  555. package/docs/research/concrete_todo_2026-02-22.md +0 -47
  556. package/docs/research/core-feature-research.md +0 -248
  557. package/docs/research/feature_uiux_roadmap_2026-02-22.md +0 -202
  558. package/docs/research/parity_matrix.md +0 -49
  559. package/docs/research/provider_expansion_candidates_2026-02-22.md +0 -80
  560. package/docs/research/provider_model_token_project_matrix_2026-02-23.md +0 -46
  561. package/docs/research/release_gate_checklist.md +0 -57
  562. package/docs/site.css +0 -505
  563. package/docs/sparkle.md +0 -28
  564. package/docs/status.md +0 -28
  565. package/docs/sync/implementation.md +0 -393
  566. package/docs/sync/readme.md +0 -317
  567. package/docs/sync_files_summary.md +0 -391
  568. package/docs/sync_quick_reference.md +0 -225
  569. package/docs/todo.md +0 -11
  570. package/docs/ui.md +0 -29
  571. package/docs/web-integration.md +0 -9
  572. package/docs/widgets.md +0 -85
  573. package/docs/workflows/error-handling-improvements.md +0 -434
  574. package/docs/workflows/folder_structure.md +0 -299
  575. package/docs/workflows/loading_states.md +0 -446
  576. package/docs/workflows/loading_states_examples.md +0 -565
  577. package/docs/workflows/loading_states_flow.md +0 -430
  578. package/docs/workflows/performance_optimizations.md +0 -457
  579. package/docs/workflows/phase1-cli-testing-report.md +0 -313
  580. package/docs/workflows/quick_reference.md +0 -228
  581. package/docs/workflows/readme.md +0 -59
  582. package/docs/workflows/recent_improvements.md +0 -275
  583. package/docs/workflows/sync_enablement_summary.md +0 -400
  584. package/docs/zai.md +0 -43
  585. package/runic.png +0 -0
  586. package/version.env +0 -2
@@ -1,1900 +0,0 @@
1
- import AppKit
2
- import RunicCore
3
- import SwiftUI
4
-
5
- /// **Menu Card Layout Metrics**
6
- /// Defines all spacing and padding constants for the menu card UI.
7
- /// These values are carefully tuned for readability and visual hierarchy.
8
- enum MenuCardMetrics {
9
- /// Horizontal padding on left/right edges of card content
10
- static let horizontalPadding: CGFloat = RunicSpacing.sm // 12
11
-
12
- /// Top padding for the header section (provider name, email)
13
- static let headerTopPadding: CGFloat = RunicSpacing.xxs // 4
14
-
15
- /// Bottom padding for the header section
16
- static let headerBottomPadding: CGFloat = RunicSpacing.xxs // 4
17
-
18
- /// Top padding for content sections (usage, credits, cost)
19
- static let sectionTopPadding: CGFloat = RunicSpacing.xs // 8
20
-
21
- /// Bottom padding for content sections
22
- static let sectionBottomPadding: CGFloat = RunicSpacing.xxs // 4
23
-
24
- /// Vertical spacing between major sections (e.g., Session vs Weekly)
25
- static let sectionSpacing: CGFloat = RunicSpacing.xs // 8
26
-
27
- /// Spacing between related blocks of content
28
- static let blockSpacing: CGFloat = RunicSpacing.xs // 8
29
-
30
- /// Spacing between individual text lines within a section
31
- static let lineSpacing: CGFloat = RunicSpacing.xxs // 4
32
-
33
- /// Base padding for menu items
34
- static let menuItemBasePadding: CGFloat = RunicSpacing.xxs // 4
35
-
36
- /// Additional padding for text with descenders (y, g, p, q, j)
37
- static let menuItemDescenderPadding: CGFloat = RunicSpacing.xxxs // 2
38
-
39
- /// Tail padding at the end of sections
40
- static let tailPadding: CGFloat = RunicSpacing.xxs // 4
41
- static let metricCardPadding: CGFloat = RunicSpacing.sm // 8
42
- static let metricCardCornerRadius: CGFloat = RunicCornerRadius.sm
43
- }
44
-
45
- /// SwiftUI card used inside the NSMenu to mirror Apple's rich menu panels.
46
- struct UsageMenuCardView: View {
47
- struct Model {
48
- enum PercentStyle: String, Sendable {
49
- case left
50
- case used
51
-
52
- var labelSuffix: String {
53
- switch self {
54
- case .left: "left"
55
- case .used: "used"
56
- }
57
- }
58
-
59
- var accessibilityLabel: String {
60
- switch self {
61
- case .left: "Usage remaining"
62
- case .used: "Usage used"
63
- }
64
- }
65
- }
66
-
67
- struct Metric: Identifiable {
68
- let id: String
69
- let title: String
70
- let percent: Double
71
- let percentStyle: PercentStyle
72
- let resetText: String?
73
- let detailText: String?
74
-
75
- var percentLabel: String {
76
- String(format: "%.0f%% %@", self.percent, self.percentStyle.labelSuffix)
77
- }
78
- }
79
-
80
- enum SubtitleStyle {
81
- case info
82
- case loading
83
- case error
84
- }
85
-
86
- struct TokenUsageSection: Sendable {
87
- let sessionLine: String
88
- let sessionDetailLine: String?
89
- let monthLine: String
90
- let monthDetailLine: String?
91
- let updatedLine: String
92
- let hintLine: String?
93
- let errorLine: String?
94
- let errorCopyText: String?
95
- }
96
-
97
- struct ProviderCostSection: Sendable {
98
- let title: String
99
- let percentUsed: Double
100
- let spendLine: String
101
- }
102
-
103
- struct InsightsSection: Sendable {
104
- let title: String
105
- let todayLine: String?
106
- let todayDetail: String?
107
- let forecastLine: String?
108
- let blockLine: String?
109
- let blockDetail: String?
110
- let modelLine: String?
111
- let projectLine: String?
112
- let projectDetail: String?
113
- let anomalyLine: String?
114
- let anomalyDetail: String?
115
- let reliabilityLine: String?
116
- let reliabilityDetail: String?
117
- let routingLine: String?
118
- let routingDetail: String?
119
- let updatedLine: String?
120
- let errorLine: String?
121
- }
122
-
123
- enum HeaderBadgeStyle {
124
- case info
125
- case warning
126
- case error
127
- }
128
-
129
- struct HeaderBadge {
130
- let text: String
131
- let style: HeaderBadgeStyle
132
- }
133
-
134
- let provider: UsageProvider
135
- let providerName: String
136
- let email: String
137
- let subtitleText: String
138
- let subtitleStyle: SubtitleStyle
139
- let planText: String?
140
- let topModelLine: String?
141
- let headerBadge: HeaderBadge?
142
- let metrics: [Metric]
143
- let usageMetricDisplayMode: UsageMetricDisplayMode
144
- let menuMode: MenuMode
145
- let creditsText: String?
146
- let creditsRemaining: Double?
147
- let creditsHintText: String?
148
- let creditsHintCopyText: String?
149
- let providerCost: ProviderCostSection?
150
- let tokenUsage: TokenUsageSection?
151
- let insights: InsightsSection?
152
- let placeholder: String?
153
- let progressColor: Color
154
- }
155
-
156
- let model: Model
157
- let width: CGFloat
158
- @Environment(\.menuItemHighlighted) private var isHighlighted
159
-
160
- init(model: Model, width: CGFloat) {
161
- self.model = model
162
- self.width = width
163
- }
164
-
165
- var body: some View {
166
- VStack(alignment: .leading, spacing: RunicSpacing.xxs) {
167
- UsageMenuCardHeaderView(model: self.model)
168
-
169
- if self.hasDetails {
170
- Divider()
171
- .padding(.vertical, RunicSpacing.xxs)
172
- }
173
-
174
- if self.model.metrics.isEmpty {
175
- if let placeholder = self.model.placeholder {
176
- MenuEmptyStateView(
177
- providerName: self.model.providerName,
178
- placeholder: placeholder,
179
- isHighlighted: self.isHighlighted)
180
- }
181
- } else {
182
- let hasUsage = !self.model.metrics.isEmpty
183
- let includeSummarySections = self.model.menuMode != .glance
184
- let includeInsightSections = self.model.menuMode == .`operator`
185
- let hasCredits = includeSummarySections && self.model.creditsText != nil
186
- let hasExtraUsage = includeSummarySections && self.model.providerCost != nil
187
- let hasTokenCost = includeSummarySections && self.model.tokenUsage != nil
188
- let hasCost = hasExtraUsage || hasTokenCost
189
- let hasInsights = includeInsightSections && self.model.insights != nil
190
-
191
- VStack(alignment: .leading, spacing: MenuCardMetrics.sectionSpacing) {
192
- if hasUsage {
193
- VStack(alignment: .leading, spacing: MenuCardMetrics.sectionSpacing) {
194
- ForEach(self.model.metrics) { metric in
195
- let displayMode = self.model.usageMetricDisplayMode
196
- UsageMenuMetricCard(
197
- metric: metric,
198
- displayMode: displayMode,
199
- tint: self.model.progressColor,
200
- isHighlighted: self.isHighlighted)
201
- }
202
- }
203
- }
204
- if hasUsage, hasCredits || hasCost || hasInsights {
205
- Divider()
206
- .padding(.vertical, RunicSpacing.xxs)
207
- }
208
- if hasCredits, let credits = self.model.creditsText {
209
- CreditsBarContent(
210
- creditsText: credits,
211
- creditsRemaining: self.model.creditsRemaining,
212
- hintText: self.model.creditsHintText,
213
- hintCopyText: self.model.creditsHintCopyText,
214
- progressColor: self.model.progressColor)
215
- }
216
- if hasCredits, hasCost || hasInsights {
217
- Divider()
218
- .padding(.vertical, RunicSpacing.xxs)
219
- }
220
- if hasExtraUsage, let providerCost = self.model.providerCost {
221
- ProviderCostContent(
222
- section: providerCost,
223
- progressColor: self.model.progressColor)
224
- }
225
- if hasExtraUsage, hasTokenCost || hasInsights {
226
- Divider()
227
- .padding(.vertical, RunicSpacing.xxs)
228
- }
229
- if hasTokenCost, let tokenUsage = self.model.tokenUsage {
230
- VStack(alignment: .leading, spacing: MenuCardMetrics.lineSpacing) {
231
- Text("Cost")
232
- .font(.body)
233
- .fontWeight(.medium)
234
- Text(tokenUsage.sessionLine)
235
- .font(.footnote)
236
- if let sessionDetail = tokenUsage.sessionDetailLine {
237
- Text(sessionDetail)
238
- .font(.footnote)
239
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
240
- }
241
- Text(tokenUsage.monthLine)
242
- .font(.footnote)
243
- if let monthDetail = tokenUsage.monthDetailLine {
244
- Text(monthDetail)
245
- .font(.footnote)
246
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
247
- }
248
- Text(tokenUsage.updatedLine)
249
- .font(.caption)
250
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
251
- if let hint = tokenUsage.hintLine, !hint.isEmpty {
252
- Text(hint)
253
- .font(.footnote)
254
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
255
- .lineLimit(4)
256
- .fixedSize(horizontal: false, vertical: true)
257
- }
258
- if let error = tokenUsage.errorLine, !error.isEmpty {
259
- HStack(alignment: .top, spacing: RunicSpacing.xs) {
260
- Image(systemName: "exclamationmark.triangle.fill")
261
- .font(.footnote)
262
- .foregroundStyle(RunicColors.error)
263
- Text(error)
264
- .font(.footnote)
265
- .foregroundStyle(MenuHighlightStyle.error(self.isHighlighted))
266
- .lineLimit(4)
267
- .fixedSize(horizontal: false, vertical: true)
268
- }
269
- .padding(RunicSpacing.xs)
270
- .background(
271
- RoundedRectangle(cornerRadius: RunicCornerRadius.sm, style: .continuous)
272
- .fill(RunicColors.error.opacity(RunicColors.Opacity.subtle)))
273
- .overlay {
274
- ClickToCopyOverlay(copyText: tokenUsage.errorCopyText ?? error)
275
- }
276
- }
277
- }
278
- }
279
- if hasCost, hasInsights {
280
- Divider()
281
- .padding(.vertical, RunicSpacing.xxs)
282
- }
283
- if hasInsights, let insights = self.model.insights {
284
- InsightsContent(section: insights)
285
- }
286
- }
287
- .padding(.bottom, self.model.creditsText == nil ? MenuCardMetrics.tailPadding : 0)
288
- }
289
- }
290
- .padding(.horizontal, MenuCardMetrics.horizontalPadding)
291
- .padding(.top, MenuCardMetrics.headerTopPadding)
292
- .padding(.bottom, MenuCardMetrics.headerBottomPadding)
293
- .frame(width: self.width, alignment: .leading)
294
- }
295
-
296
- private var hasDetails: Bool {
297
- let includeSummarySections = self.model.menuMode != .glance
298
- let includeInsightSections = self.model.menuMode == .`operator`
299
- return !self.model.metrics.isEmpty || self.model.placeholder != nil || (includeSummarySections && (
300
- self.model.tokenUsage != nil || self.model.providerCost != nil)) || (includeInsightSections &&
301
- self.model.insights != nil)
302
- }
303
- }
304
-
305
- private struct UsageMenuCardHeaderView: View {
306
- let model: UsageMenuCardView.Model
307
- @Environment(\.menuItemHighlighted) private var isHighlighted
308
-
309
- var body: some View {
310
- VStack(alignment: .leading, spacing: RunicSpacing.xs) {
311
- HStack(alignment: .center, spacing: RunicSpacing.sm) {
312
- ProviderAvatarView(provider: self.model.provider)
313
-
314
- VStack(alignment: .leading, spacing: RunicSpacing.xxxs) {
315
- Text(self.model.providerName)
316
- .font(.headline)
317
- .fontWeight(.semibold)
318
- if !self.model.email.isEmpty {
319
- ProfilePill(
320
- text: self.model.email,
321
- systemImage: "person.crop.circle",
322
- tint: self.brandAccent)
323
- .lineLimit(1)
324
- }
325
- }
326
-
327
- Spacer()
328
-
329
- VStack(alignment: .trailing, spacing: RunicSpacing.xxxs) {
330
- if let badge = self.model.headerBadge {
331
- MenuHeaderBadgeView(badge: badge, isHighlighted: self.isHighlighted)
332
- }
333
- if let plan = self.model.planText {
334
- ProfilePill(
335
- text: plan,
336
- systemImage: "sparkles",
337
- tint: self.brandAccent,
338
- style: .plan)
339
- .lineLimit(1)
340
- }
341
- }
342
- }
343
-
344
- let subtitleAlignment: VerticalAlignment = self.model.subtitleStyle == .error ? .top : .firstTextBaseline
345
- HStack(alignment: subtitleAlignment) {
346
- Text(self.model.subtitleText)
347
- .font(.footnote.weight(.medium))
348
- .foregroundStyle(self.subtitleColor)
349
- .lineLimit(self.model.subtitleStyle == .error ? 4 : 1)
350
- .multilineTextAlignment(.leading)
351
- .fixedSize(horizontal: false, vertical: true)
352
- .layoutPriority(1)
353
- .padding(.bottom, self.model.subtitleStyle == .error ? 4 : 0)
354
- Spacer()
355
- if self.model.subtitleStyle == .error, !self.model.subtitleText.isEmpty {
356
- CopyIconButton(copyText: self.model.subtitleText, isHighlighted: self.isHighlighted)
357
- }
358
- }
359
-
360
- if let topModelLine = self.model.topModelLine {
361
- Text(topModelLine)
362
- .font(.footnote)
363
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
364
- .lineLimit(1)
365
- }
366
- }
367
- .padding(.horizontal, RunicSpacing.xs)
368
- .padding(.vertical, RunicSpacing.xs)
369
- .background(self.headerBackground)
370
- .overlay(self.headerBorder)
371
- }
372
-
373
- private var subtitleColor: Color {
374
- switch self.model.subtitleStyle {
375
- case .info: MenuHighlightStyle.secondary(self.isHighlighted)
376
- case .loading: MenuHighlightStyle.secondary(self.isHighlighted)
377
- case .error: MenuHighlightStyle.error(self.isHighlighted)
378
- }
379
- }
380
-
381
- private var brandAccent: Color {
382
- let color = ProviderDescriptorRegistry.descriptor(for: self.model.provider).branding.color
383
- return Color(red: color.red, green: color.green, blue: color.blue)
384
- }
385
-
386
- private var headerBackground: some View {
387
- let base = self.brandNSColor
388
- let top = base.blended(withFraction: 0.35, of: .white) ?? base
389
- let bottom = base.blended(withFraction: 0.25, of: .black) ?? base
390
- let start = Color(nsColor: top).opacity(RunicColors.Opacity.medium)
391
- let end = Color(nsColor: bottom).opacity(RunicColors.Opacity.light)
392
- return RoundedRectangle(cornerRadius: RunicCornerRadius.lg, style: .continuous)
393
- .fill(LinearGradient(colors: [start, end], startPoint: .topLeading, endPoint: .bottomTrailing))
394
- }
395
-
396
- private var headerBorder: some View {
397
- return RoundedRectangle(cornerRadius: RunicCornerRadius.lg, style: .continuous)
398
- .stroke(Color.white.opacity(RunicColors.Opacity.medium), lineWidth: 0.7)
399
- }
400
-
401
- private var brandNSColor: NSColor {
402
- let color = ProviderDescriptorRegistry.descriptor(for: self.model.provider).branding.color
403
- return NSColor(calibratedRed: CGFloat(color.red), green: CGFloat(color.green), blue: CGFloat(color.blue), alpha: 1)
404
- }
405
- }
406
-
407
- private struct ProviderAvatarView: View {
408
- let provider: UsageProvider
409
-
410
- var body: some View {
411
- let size: CGFloat = 40
412
- let base = self.brandNSColor
413
- let top = base.blended(withFraction: 0.35, of: .white) ?? base
414
- let mid = base.blended(withFraction: 0.15, of: .black) ?? base
415
- let bottom = base.blended(withFraction: 0.35, of: .black) ?? base
416
-
417
- let coreGradient = LinearGradient(
418
- colors: [Color(nsColor: top), Color(nsColor: mid), Color(nsColor: bottom)],
419
- startPoint: .topLeading,
420
- endPoint: .bottomTrailing)
421
-
422
- let ringGradient = AngularGradient(
423
- colors: [
424
- Color.white.opacity(RunicColors.Opacity.vivid),
425
- Color(nsColor: top).opacity(RunicColors.Opacity.emphasis),
426
- Color.white.opacity(RunicColors.Opacity.medium),
427
- Color(nsColor: bottom).opacity(RunicColors.Opacity.prominent),
428
- Color.white.opacity(RunicColors.Opacity.vivid),
429
- ],
430
- center: .center)
431
-
432
- ZStack {
433
- RoundedRectangle(cornerRadius: RunicCornerRadius.lg, style: .continuous)
434
- .fill(coreGradient)
435
- .shadow(color: Color(nsColor: base).opacity(0.25), radius: 6, x: 0, y: 4)
436
-
437
- RoundedRectangle(cornerRadius: RunicCornerRadius.lg, style: .continuous)
438
- .stroke(ringGradient, lineWidth: 1.2)
439
-
440
- RoundedRectangle(cornerRadius: RunicCornerRadius.lg - 2, style: .continuous)
441
- .fill(
442
- LinearGradient(
443
- colors: [Color.white.opacity(0.45), .clear],
444
- startPoint: .topLeading,
445
- endPoint: .bottomTrailing))
446
- .padding(RunicSpacing.xxxs)
447
- .blendMode(.screen)
448
-
449
- if let icon = ProviderBrandIcon.templateImage(for: self.provider, size: 18) {
450
- Image(nsImage: icon)
451
- .resizable()
452
- .scaledToFit()
453
- .frame(width: 18, height: 18)
454
- .foregroundStyle(Color.white.opacity(0.95))
455
- .shadow(color: Color.black.opacity(0.18), radius: 1, x: 0, y: 1)
456
- } else {
457
- Text(self.fallbackInitials)
458
- .font(.caption.weight(.semibold))
459
- .foregroundStyle(Color.white.opacity(0.95))
460
- }
461
- }
462
- .frame(width: size, height: size)
463
- .overlay(
464
- RoundedRectangle(cornerRadius: RunicCornerRadius.lg, style: .continuous)
465
- .stroke(Color.white.opacity(RunicColors.Opacity.medium), lineWidth: 0.6))
466
- }
467
-
468
- private var brandNSColor: NSColor {
469
- let color = ProviderDescriptorRegistry.descriptor(for: self.provider).branding.color
470
- return NSColor(calibratedRed: CGFloat(color.red), green: CGFloat(color.green), blue: CGFloat(color.blue), alpha: 1)
471
- }
472
-
473
- private var fallbackInitials: String {
474
- let name = ProviderDescriptorRegistry.descriptor(for: self.provider).metadata.displayName
475
- let parts = name.split(separator: " ")
476
- if parts.count >= 2 {
477
- return "\(parts[0].prefix(1))\(parts[1].prefix(1))"
478
- }
479
- return String(name.prefix(2)).uppercased()
480
- }
481
- }
482
-
483
- private struct ProfilePill: View {
484
- enum Style { case email, plan }
485
-
486
- let text: String
487
- let systemImage: String
488
- let tint: Color
489
- var style: Style = .email
490
-
491
- var body: some View {
492
- HStack(spacing: RunicSpacing.xxxs) {
493
- Image(systemName: self.systemImage)
494
- .font(.caption2.weight(.semibold))
495
- Text(self.text)
496
- .font(.caption.weight(.medium))
497
- }
498
- .padding(.horizontal, RunicSpacing.compact)
499
- .padding(.vertical, RunicSpacing.xxxs)
500
- .foregroundStyle(self.foregroundColor)
501
- .background(
502
- RoundedRectangle(cornerRadius: RunicCornerRadius.sm, style: .continuous)
503
- .fill(self.backgroundColor))
504
- .overlay(
505
- RoundedRectangle(cornerRadius: RunicCornerRadius.sm, style: .continuous)
506
- .stroke(self.borderColor, lineWidth: 0.5))
507
- }
508
-
509
- private var foregroundColor: Color {
510
- switch self.style {
511
- case .email:
512
- return .secondary
513
- case .plan:
514
- return self.tint.opacity(RunicColors.Opacity.vivid)
515
- }
516
- }
517
-
518
- private var backgroundColor: Color {
519
- switch self.style {
520
- case .email:
521
- return Color(nsColor: .quaternaryLabelColor).opacity(RunicColors.Opacity.emphasis)
522
- case .plan:
523
- return self.tint.opacity(RunicColors.Opacity.light)
524
- }
525
- }
526
-
527
- private var borderColor: Color {
528
- switch self.style {
529
- case .email:
530
- return Color(nsColor: .separatorColor).opacity(RunicColors.Opacity.strong)
531
- case .plan:
532
- return self.tint.opacity(RunicColors.Opacity.medium)
533
- }
534
- }
535
- }
536
-
537
-
538
- private struct MenuHeaderBadgeView: View {
539
- let badge: UsageMenuCardView.Model.HeaderBadge
540
- let isHighlighted: Bool
541
-
542
- var body: some View {
543
- HStack(spacing: RunicSpacing.xxs) {
544
- if self.badge.style == .info {
545
- ProgressView()
546
- .controlSize(.mini)
547
- .scaleEffect(0.7)
548
- .frame(width: 10, height: 10)
549
- } else {
550
- Image(systemName: self.badgeIcon)
551
- .font(.caption2.weight(.semibold))
552
- }
553
- Text(self.badge.text)
554
- .font(.caption2.weight(.semibold))
555
- }
556
- .padding(.horizontal, RunicSpacing.compact)
557
- .padding(.vertical, RunicSpacing.xxxs)
558
- .foregroundStyle(self.foregroundColor)
559
- .background(
560
- RoundedRectangle(cornerRadius: RunicCornerRadius.sm, style: .continuous)
561
- .fill(self.backgroundColor))
562
- .overlay(
563
- RoundedRectangle(cornerRadius: RunicCornerRadius.sm, style: .continuous)
564
- .stroke(self.borderColor, lineWidth: 0.5))
565
- }
566
-
567
- private var badgeIcon: String {
568
- switch self.badge.style {
569
- case .info: return "arrow.triangle.2.circlepath"
570
- case .warning: return "exclamationmark.triangle.fill"
571
- case .error: return "xmark.circle.fill"
572
- }
573
- }
574
-
575
- private var backgroundColor: Color {
576
- switch self.badge.style {
577
- case .info:
578
- return Color(nsColor: .systemBlue).opacity(self.isHighlighted ? 0.25 : 0.12)
579
- case .warning:
580
- return Color(nsColor: .systemOrange).opacity(self.isHighlighted ? 0.35 : 0.15)
581
- case .error:
582
- return Color(nsColor: .systemRed).opacity(self.isHighlighted ? 0.35 : 0.15)
583
- }
584
- }
585
-
586
- private var foregroundColor: Color {
587
- switch self.badge.style {
588
- case .info:
589
- return Color(nsColor: .systemBlue).opacity(RunicColors.Opacity.vivid)
590
- case .warning:
591
- return Color(nsColor: .systemOrange)
592
- case .error:
593
- return Color(nsColor: .systemRed)
594
- }
595
- }
596
-
597
- private var borderColor: Color {
598
- switch self.badge.style {
599
- case .info:
600
- return Color(nsColor: .systemBlue).opacity(RunicColors.Opacity.medium)
601
- case .warning:
602
- return Color(nsColor: .systemOrange).opacity(RunicColors.Opacity.strong)
603
- case .error:
604
- return Color(nsColor: .systemRed).opacity(RunicColors.Opacity.strong)
605
- }
606
- }
607
- }
608
-
609
- private struct MenuEmptyStateView: View {
610
- let providerName: String
611
- let placeholder: String
612
- let isHighlighted: Bool
613
-
614
- var body: some View {
615
- VStack(alignment: .leading, spacing: RunicSpacing.xxs) {
616
- HStack(spacing: RunicSpacing.xxs) {
617
- Image(systemName: "sparkles")
618
- .font(.caption.weight(.semibold))
619
- Text("Connect \(self.providerName)")
620
- .font(.subheadline.weight(.semibold))
621
- }
622
- Text(self.placeholder)
623
- .font(.footnote)
624
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
625
- Text("Open Settings → Providers, add credentials, then refresh.")
626
- .font(.footnote)
627
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
628
- .fixedSize(horizontal: false, vertical: true)
629
- }
630
- }
631
- }
632
-
633
- private struct CopyIconButtonStyle: ButtonStyle {
634
- let isHighlighted: Bool
635
-
636
- func makeBody(configuration: Configuration) -> some View {
637
- configuration.label
638
- .padding(RunicSpacing.xxs)
639
- .background {
640
- RoundedRectangle(cornerRadius: RunicCornerRadius.xs, style: .continuous)
641
- .fill(MenuHighlightStyle.secondary(self.isHighlighted).opacity(configuration.isPressed ? 0.18 : 0))
642
- }
643
- .scaleEffect(configuration.isPressed ? 0.94 : 1)
644
- .animation(RunicAnimation.highlight, value: configuration.isPressed)
645
- }
646
- }
647
-
648
- private struct CopyIconButton: View {
649
- let copyText: String
650
- let isHighlighted: Bool
651
-
652
- @State private var didCopy = false
653
- @State private var resetTask: Task<Void, Never>?
654
-
655
- var body: some View {
656
- Button {
657
- self.copyToPasteboard()
658
- withAnimation(RunicAnimation.highlight) {
659
- self.didCopy = true
660
- }
661
- self.resetTask?.cancel()
662
- self.resetTask = Task { @MainActor in
663
- try? await Task.sleep(for: .seconds(0.9))
664
- withAnimation(.easeOut(duration: 0.2)) {
665
- self.didCopy = false
666
- }
667
- }
668
- } label: {
669
- Image(systemName: self.didCopy ? "checkmark" : "doc.on.doc")
670
- .font(.caption2.weight(.semibold))
671
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
672
- .frame(width: 18, height: 18)
673
- }
674
- .buttonStyle(CopyIconButtonStyle(isHighlighted: self.isHighlighted))
675
- .accessibilityLabel(self.didCopy ? "Copied" : "Copy error")
676
- }
677
-
678
- private func copyToPasteboard() {
679
- let pb = NSPasteboard.general
680
- pb.clearContents()
681
- pb.setString(self.copyText, forType: .string)
682
- }
683
- }
684
-
685
- private struct ProviderCostContent: View {
686
- let section: UsageMenuCardView.Model.ProviderCostSection
687
- let progressColor: Color
688
- @Environment(\.menuItemHighlighted) private var isHighlighted
689
-
690
- var body: some View {
691
- VStack(alignment: .leading, spacing: MenuCardMetrics.lineSpacing) {
692
- Text(self.section.title)
693
- .font(.body)
694
- .fontWeight(.medium)
695
- UsageProgressBar(
696
- percent: self.section.percentUsed,
697
- tint: self.progressColor,
698
- accessibilityLabel: "Extra usage spent")
699
- HStack(alignment: .firstTextBaseline) {
700
- Text(self.section.spendLine)
701
- .font(.footnote)
702
- Spacer()
703
- Text(String(format: "%.0f%% used", min(100, max(0, self.section.percentUsed))))
704
- .font(.system(.footnote, design: .rounded))
705
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
706
- }
707
- }
708
- }
709
- }
710
-
711
- private struct InsightsContent: View {
712
- let section: UsageMenuCardView.Model.InsightsSection
713
- @Environment(\.menuItemHighlighted) private var isHighlighted
714
-
715
- var body: some View {
716
- VStack(alignment: .leading, spacing: MenuCardMetrics.lineSpacing) {
717
- Text(self.section.title)
718
- .font(.body)
719
- .fontWeight(.medium)
720
- if let today = self.section.todayLine {
721
- Text(today)
722
- .font(.footnote)
723
- }
724
- if let detail = self.section.todayDetail {
725
- Text(detail)
726
- .font(.footnote)
727
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
728
- }
729
- if let forecastLine = self.section.forecastLine {
730
- Text(forecastLine)
731
- .font(.footnote)
732
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
733
- }
734
- if let block = self.section.blockLine {
735
- Text(block)
736
- .font(.footnote)
737
- }
738
- if let blockDetail = self.section.blockDetail {
739
- Text(blockDetail)
740
- .font(.footnote)
741
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
742
- }
743
- if let modelLine = self.section.modelLine {
744
- Text(modelLine)
745
- .font(.footnote)
746
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
747
- }
748
- if let projectLine = self.section.projectLine {
749
- Text(projectLine)
750
- .font(.footnote)
751
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
752
- }
753
- if let projectDetail = self.section.projectDetail {
754
- Text(projectDetail)
755
- .font(.footnote)
756
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
757
- }
758
- if let anomalyLine = self.section.anomalyLine {
759
- Text(anomalyLine)
760
- .font(.footnote)
761
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
762
- }
763
- if let anomalyDetail = self.section.anomalyDetail {
764
- Text(anomalyDetail)
765
- .font(.footnote)
766
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
767
- }
768
- if let reliabilityLine = self.section.reliabilityLine {
769
- Text(reliabilityLine)
770
- .font(.footnote)
771
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
772
- }
773
- if let reliabilityDetail = self.section.reliabilityDetail {
774
- Text(reliabilityDetail)
775
- .font(.footnote)
776
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
777
- }
778
- if let routingLine = self.section.routingLine {
779
- Text(routingLine)
780
- .font(.footnote)
781
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
782
- }
783
- if let routingDetail = self.section.routingDetail {
784
- Text(routingDetail)
785
- .font(.footnote)
786
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
787
- }
788
- if let updated = self.section.updatedLine {
789
- Text(updated)
790
- .font(.caption)
791
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
792
- }
793
- if let error = self.section.errorLine {
794
- HStack(alignment: .top, spacing: RunicSpacing.xs) {
795
- Image(systemName: "exclamationmark.triangle.fill")
796
- .font(.footnote)
797
- .foregroundStyle(RunicColors.error)
798
- Text(error)
799
- .font(.footnote)
800
- .foregroundStyle(MenuHighlightStyle.error(self.isHighlighted))
801
- .lineLimit(4)
802
- .fixedSize(horizontal: false, vertical: true)
803
- }
804
- .padding(RunicSpacing.xs)
805
- .background(
806
- RoundedRectangle(cornerRadius: RunicCornerRadius.sm, style: .continuous)
807
- .fill(RunicColors.error.opacity(RunicColors.Opacity.subtle)))
808
- }
809
- }
810
- }
811
- }
812
-
813
- struct UsageMenuCardHeaderSectionView: View {
814
- let model: UsageMenuCardView.Model
815
- let showDivider: Bool
816
- let width: CGFloat
817
-
818
- var body: some View {
819
- VStack(alignment: .leading, spacing: RunicSpacing.xxs) {
820
- UsageMenuCardHeaderView(model: self.model)
821
-
822
- if self.showDivider {
823
- Divider()
824
- .padding(.vertical, RunicSpacing.xxs)
825
- }
826
- }
827
- .padding(.horizontal, MenuCardMetrics.horizontalPadding)
828
- .padding(.top, MenuCardMetrics.headerTopPadding)
829
- .padding(.bottom, self.model.subtitleStyle == .error ? MenuCardMetrics.headerBottomPadding : 0)
830
- .frame(width: self.width, alignment: .leading)
831
- }
832
- }
833
-
834
- private struct UsageMenuMetricCard: View {
835
- let metric: UsageMenuCardView.Model.Metric
836
- let displayMode: UsageMetricDisplayMode
837
- let tint: Color
838
- let isHighlighted: Bool
839
-
840
- var body: some View {
841
- VStack(alignment: .leading, spacing: MenuCardMetrics.lineSpacing) {
842
- HStack(alignment: .firstTextBaseline, spacing: RunicSpacing.xs) {
843
- Text(self.metric.title)
844
- .font(.caption.weight(.semibold))
845
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
846
- Spacer(minLength: RunicSpacing.xs)
847
- if let reset = self.metric.resetText {
848
- Text(reset)
849
- .font(.caption2)
850
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
851
- }
852
- }
853
-
854
- if self.displayMode.showsBars {
855
- UsageProgressBar(
856
- percent: self.metric.percent,
857
- tint: self.tint,
858
- accessibilityLabel: self.metric.percentStyle.accessibilityLabel)
859
- }
860
-
861
- if self.displayMode.showsPercent {
862
- HStack(alignment: .firstTextBaseline, spacing: RunicSpacing.xs) {
863
- Text(self.metric.percentLabel)
864
- .font(.system(.caption, design: .rounded))
865
- .fontWeight(.medium)
866
- Spacer()
867
- }
868
- }
869
-
870
- if let detail = self.metric.detailText {
871
- Text(detail)
872
- .font(.caption)
873
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
874
- .lineLimit(1)
875
- .textCase(.none)
876
- }
877
- }
878
- .padding(MenuCardMetrics.metricCardPadding)
879
- .background(
880
- RoundedRectangle(
881
- cornerRadius: MenuCardMetrics.metricCardCornerRadius,
882
- style: .continuous)
883
- .fill(Color(nsColor: .controlBackgroundColor).opacity(self.isHighlighted ? 0.2 : 0.14))
884
- )
885
- .overlay(
886
- RoundedRectangle(
887
- cornerRadius: MenuCardMetrics.metricCardCornerRadius,
888
- style: .continuous)
889
- .strokeBorder(Color(nsColor: .separatorColor).opacity(0.25), lineWidth: 1)
890
- )
891
- }
892
- }
893
-
894
- struct UsageMenuCardUsageSectionView: View {
895
- let model: UsageMenuCardView.Model
896
- let showBottomDivider: Bool
897
- let bottomPadding: CGFloat
898
- let width: CGFloat
899
- @Environment(\.menuItemHighlighted) private var isHighlighted
900
-
901
- var body: some View {
902
- VStack(alignment: .leading, spacing: MenuCardMetrics.sectionSpacing) {
903
- if self.model.metrics.isEmpty {
904
- if let placeholder = self.model.placeholder {
905
- MenuEmptyStateView(
906
- providerName: self.model.providerName,
907
- placeholder: placeholder,
908
- isHighlighted: self.isHighlighted)
909
- }
910
- } else {
911
- ForEach(self.model.metrics) { metric in
912
- let displayMode = self.model.usageMetricDisplayMode
913
- UsageMenuMetricCard(
914
- metric: metric,
915
- displayMode: displayMode,
916
- tint: self.model.progressColor,
917
- isHighlighted: self.isHighlighted)
918
- }
919
- }
920
- if self.showBottomDivider {
921
- Divider()
922
- .padding(.vertical, RunicSpacing.xxs)
923
- }
924
- }
925
- .padding(.horizontal, MenuCardMetrics.horizontalPadding)
926
- .padding(.top, MenuCardMetrics.sectionTopPadding)
927
- .padding(.bottom, self.bottomPadding)
928
- .frame(width: self.width, alignment: .leading)
929
- }
930
- }
931
-
932
- struct UsageMenuCardCreditsSectionView: View {
933
- let model: UsageMenuCardView.Model
934
- let showBottomDivider: Bool
935
- let topPadding: CGFloat
936
- let bottomPadding: CGFloat
937
- let width: CGFloat
938
-
939
- init(
940
- model: UsageMenuCardView.Model,
941
- showBottomDivider: Bool,
942
- topPadding: CGFloat,
943
- bottomPadding: CGFloat,
944
- width: CGFloat)
945
- {
946
- self.model = model
947
- self.showBottomDivider = showBottomDivider
948
- self.topPadding = topPadding
949
- self.bottomPadding = bottomPadding
950
- self.width = width
951
- }
952
-
953
- var body: some View {
954
- if let credits = self.model.creditsText {
955
- VStack(alignment: .leading, spacing: MenuCardMetrics.lineSpacing) {
956
- CreditsBarContent(
957
- creditsText: credits,
958
- creditsRemaining: self.model.creditsRemaining,
959
- hintText: self.model.creditsHintText,
960
- hintCopyText: self.model.creditsHintCopyText,
961
- progressColor: self.model.progressColor)
962
- if self.showBottomDivider {
963
- Divider()
964
- .padding(.vertical, RunicSpacing.xxs)
965
- }
966
- }
967
- .padding(.horizontal, MenuCardMetrics.horizontalPadding)
968
- .padding(.top, self.topPadding)
969
- .padding(.bottom, self.bottomPadding)
970
- .frame(width: self.width, alignment: .leading)
971
- }
972
- }
973
- }
974
-
975
- private struct CreditsBarContent: View {
976
- private static let fullScaleTokens: Double = 1000
977
-
978
- let creditsText: String
979
- let creditsRemaining: Double?
980
- let hintText: String?
981
- let hintCopyText: String?
982
- let progressColor: Color
983
- @Environment(\.menuItemHighlighted) private var isHighlighted
984
-
985
- private var percentLeft: Double? {
986
- guard let creditsRemaining else { return nil }
987
- let percent = (creditsRemaining / Self.fullScaleTokens) * 100
988
- return min(100, max(0, percent))
989
- }
990
-
991
- private var scaleText: String {
992
- let scale = UsageFormatter.tokenCountString(Int(Self.fullScaleTokens))
993
- return "\(scale) tokens"
994
- }
995
-
996
- var body: some View {
997
- VStack(alignment: .leading, spacing: MenuCardMetrics.lineSpacing) {
998
- Text("Credits")
999
- .font(.body)
1000
- .fontWeight(.medium)
1001
- if let percentLeft {
1002
- UsageProgressBar(
1003
- percent: percentLeft,
1004
- tint: self.progressColor,
1005
- accessibilityLabel: "Credits remaining")
1006
- HStack(alignment: .firstTextBaseline) {
1007
- Text(self.creditsText)
1008
- .font(.system(.caption, design: .rounded))
1009
- Spacer()
1010
- Text(self.scaleText)
1011
- .font(.system(.caption, design: .rounded))
1012
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
1013
- }
1014
- } else {
1015
- Text(self.creditsText)
1016
- .font(.system(.caption, design: .rounded))
1017
- }
1018
- if let hintText, !hintText.isEmpty {
1019
- Text(hintText)
1020
- .font(.footnote)
1021
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
1022
- .lineLimit(4)
1023
- .fixedSize(horizontal: false, vertical: true)
1024
- .overlay {
1025
- ClickToCopyOverlay(copyText: self.hintCopyText ?? hintText)
1026
- }
1027
- }
1028
- }
1029
- }
1030
- }
1031
-
1032
- struct UsageMenuCardInsightsSectionView: View {
1033
- let model: UsageMenuCardView.Model
1034
- let topPadding: CGFloat
1035
- let bottomPadding: CGFloat
1036
- let width: CGFloat
1037
-
1038
- var body: some View {
1039
- if let insights = self.model.insights {
1040
- InsightsContent(section: insights)
1041
- .padding(.horizontal, MenuCardMetrics.horizontalPadding)
1042
- .padding(.top, self.topPadding)
1043
- .padding(.bottom, self.bottomPadding)
1044
- .frame(width: self.width, alignment: .leading)
1045
- }
1046
- }
1047
- }
1048
-
1049
- struct UsageMenuCardCostSectionView: View {
1050
- let model: UsageMenuCardView.Model
1051
- let topPadding: CGFloat
1052
- let bottomPadding: CGFloat
1053
- let width: CGFloat
1054
- @Environment(\.menuItemHighlighted) private var isHighlighted
1055
-
1056
- var body: some View {
1057
- let hasTokenCost = self.model.tokenUsage != nil
1058
- return Group {
1059
- if hasTokenCost {
1060
- VStack(alignment: .leading, spacing: MenuCardMetrics.blockSpacing) {
1061
- if let tokenUsage = self.model.tokenUsage {
1062
- VStack(alignment: .leading, spacing: MenuCardMetrics.lineSpacing) {
1063
- Text("Cost")
1064
- .font(.body)
1065
- .fontWeight(.medium)
1066
- Text(tokenUsage.sessionLine)
1067
- .font(.system(.caption, design: .rounded))
1068
- if let sessionDetail = tokenUsage.sessionDetailLine {
1069
- Text(sessionDetail)
1070
- .font(.footnote)
1071
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
1072
- }
1073
- Text(tokenUsage.monthLine)
1074
- .font(.system(.caption, design: .rounded))
1075
- if let monthDetail = tokenUsage.monthDetailLine {
1076
- Text(monthDetail)
1077
- .font(.footnote)
1078
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
1079
- }
1080
- Text(tokenUsage.updatedLine)
1081
- .font(.caption)
1082
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
1083
- if let hint = tokenUsage.hintLine, !hint.isEmpty {
1084
- Text(hint)
1085
- .font(.footnote)
1086
- .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
1087
- .lineLimit(4)
1088
- .fixedSize(horizontal: false, vertical: true)
1089
- }
1090
- if let error = tokenUsage.errorLine, !error.isEmpty {
1091
- HStack(alignment: .top, spacing: RunicSpacing.xs) {
1092
- Image(systemName: "exclamationmark.triangle.fill")
1093
- .font(.footnote)
1094
- .foregroundStyle(RunicColors.error)
1095
- Text(error)
1096
- .font(.footnote)
1097
- .foregroundStyle(MenuHighlightStyle.error(self.isHighlighted))
1098
- .lineLimit(4)
1099
- .fixedSize(horizontal: false, vertical: true)
1100
- }
1101
- .padding(RunicSpacing.xs)
1102
- .background(
1103
- RoundedRectangle(cornerRadius: RunicCornerRadius.sm, style: .continuous)
1104
- .fill(RunicColors.error.opacity(RunicColors.Opacity.subtle)))
1105
- .overlay {
1106
- ClickToCopyOverlay(copyText: tokenUsage.errorCopyText ?? error)
1107
- }
1108
- }
1109
- }
1110
- }
1111
- }
1112
- .padding(.horizontal, MenuCardMetrics.horizontalPadding)
1113
- .padding(.top, self.topPadding)
1114
- .padding(.bottom, self.bottomPadding)
1115
- .frame(width: self.width, alignment: .leading)
1116
- }
1117
- }
1118
- }
1119
- }
1120
-
1121
- struct UsageMenuCardExtraUsageSectionView: View {
1122
- let model: UsageMenuCardView.Model
1123
- let topPadding: CGFloat
1124
- let bottomPadding: CGFloat
1125
- let width: CGFloat
1126
-
1127
- var body: some View {
1128
- Group {
1129
- if let providerCost = self.model.providerCost {
1130
- ProviderCostContent(
1131
- section: providerCost,
1132
- progressColor: self.model.progressColor)
1133
- .padding(.horizontal, MenuCardMetrics.horizontalPadding)
1134
- .padding(.top, self.topPadding)
1135
- .padding(.bottom, self.bottomPadding)
1136
- .frame(width: self.width, alignment: .leading)
1137
- }
1138
- }
1139
- }
1140
- }
1141
-
1142
- // MARK: - Model factory
1143
-
1144
- extension UsageMenuCardView.Model {
1145
- struct Input {
1146
- let provider: UsageProvider
1147
- let metadata: ProviderMetadata
1148
- let snapshot: UsageSnapshot?
1149
- let credits: CreditsSnapshot?
1150
- let creditsError: String?
1151
- let dashboard: OpenAIDashboardSnapshot?
1152
- let dashboardError: String?
1153
- let tokenSnapshot: CostUsageTokenSnapshot?
1154
- let tokenError: String?
1155
- let ledgerDaily: UsageLedgerDailySummary?
1156
- let ledgerActiveBlock: UsageLedgerBlockSummary?
1157
- let ledgerTopModel: UsageLedgerModelSummary?
1158
- let ledgerTopProject: UsageLedgerProjectSummary?
1159
- let ledgerSpendForecast: UsageLedgerSpendForecast?
1160
- let ledgerTopProjectSpendForecast: UsageLedgerSpendForecast?
1161
- let ledgerAnomaly: UsageLedgerAnomalySummary?
1162
- let ledgerReliability: UsageLedgerReliabilityScore?
1163
- let ledgerRouting: UsageLedgerRoutingRecommendation?
1164
- let ledgerError: String?
1165
- let ledgerUpdatedAt: Date?
1166
- let account: AccountInfo
1167
- let isRefreshing: Bool
1168
- let lastError: String?
1169
- let usageBarsShowUsed: Bool
1170
- let usageMetricDisplayMode: UsageMetricDisplayMode
1171
- let menuMode: MenuMode
1172
- let tokenCostUsageEnabled: Bool
1173
- let showOptionalCreditsAndExtraUsage: Bool
1174
- let now: Date
1175
- }
1176
-
1177
- static func make(_ input: Input) -> UsageMenuCardView.Model {
1178
- let trimmedError = input.lastError?.trimmingCharacters(in: .whitespacesAndNewlines)
1179
- let normalizedError = (trimmedError?.isEmpty ?? true) ? nil : trimmedError
1180
- let email = Self.email(
1181
- for: input.provider,
1182
- snapshot: input.snapshot,
1183
- account: input.account,
1184
- metadata: input.metadata)
1185
- let planText = Self.plan(
1186
- for: input.provider,
1187
- snapshot: input.snapshot,
1188
- account: input.account,
1189
- metadata: input.metadata)
1190
- let metrics = Self.metrics(input: input)
1191
- let creditsText: String? = if input.provider == .codex, !input.showOptionalCreditsAndExtraUsage {
1192
- nil
1193
- } else {
1194
- Self.creditsLine(metadata: input.metadata, credits: input.credits, error: input.creditsError)
1195
- }
1196
- let creditsHintText = Self.dashboardHint(provider: input.provider, error: input.dashboardError)
1197
- let providerCost: ProviderCostSection? = if input.provider == .claude, !input.showOptionalCreditsAndExtraUsage {
1198
- nil
1199
- } else {
1200
- Self.providerCostSection(provider: input.provider, cost: input.snapshot?.providerCost)
1201
- }
1202
- let tokenUsage = Self.tokenUsageSection(
1203
- provider: input.provider,
1204
- enabled: input.tokenCostUsageEnabled,
1205
- snapshot: input.tokenSnapshot,
1206
- error: input.tokenError)
1207
- let topModelLine = Self.topModelLine(input.ledgerTopModel)
1208
- let insights = Self.removingModelLine(from: Self.insightsSection(input: input), when: topModelLine != nil)
1209
- let subtitle = Self.subtitle(
1210
- snapshot: input.snapshot,
1211
- isRefreshing: input.isRefreshing,
1212
- lastError: normalizedError)
1213
- let headerBadge: HeaderBadge? = if input.isRefreshing {
1214
- HeaderBadge(text: "Refreshing", style: .info)
1215
- } else if normalizedError != nil {
1216
- HeaderBadge(text: "Issue", style: .error)
1217
- } else {
1218
- nil
1219
- }
1220
- let placeholder = input.snapshot == nil && !input.isRefreshing && normalizedError == nil ? "No usage yet" : nil
1221
-
1222
- return UsageMenuCardView.Model(
1223
- provider: input.provider,
1224
- providerName: input.metadata.displayName,
1225
- email: email,
1226
- subtitleText: subtitle.text,
1227
- subtitleStyle: subtitle.style,
1228
- planText: planText,
1229
- topModelLine: topModelLine,
1230
- headerBadge: headerBadge,
1231
- metrics: metrics,
1232
- usageMetricDisplayMode: input.usageMetricDisplayMode,
1233
- menuMode: input.menuMode,
1234
- creditsText: creditsText,
1235
- creditsRemaining: input.credits?.remaining,
1236
- creditsHintText: creditsHintText,
1237
- creditsHintCopyText: (input.dashboardError?.isEmpty ?? true) ? nil : input.dashboardError,
1238
- providerCost: providerCost,
1239
- tokenUsage: tokenUsage,
1240
- insights: insights,
1241
- placeholder: placeholder,
1242
- progressColor: Self.progressColor(for: input.provider))
1243
- }
1244
-
1245
- private static func topModelLine(_ summary: UsageLedgerModelSummary?) -> String? {
1246
- guard let summary else { return nil }
1247
- let modelName = UsageFormatter.modelDisplayName(summary.model)
1248
- let tokens = UsageFormatter.tokenCountString(summary.totals.totalTokens)
1249
- var parts = ["Top model: \(modelName)", "\(tokens) tokens", "\(summary.entryCount) req"]
1250
- if let context = UsageFormatter.modelContextLabel(for: summary.model) {
1251
- parts.append(context)
1252
- }
1253
- if let cost = summary.totals.costUSD {
1254
- parts.append(UsageFormatter.usdString(cost))
1255
- if let per1K = UsageFormatter.usdPer1KTokensString(
1256
- costUSD: cost,
1257
- tokenCount: summary.totals.totalTokens)
1258
- {
1259
- parts.append(per1K)
1260
- }
1261
- }
1262
- return parts.joined(separator: " · ")
1263
- }
1264
-
1265
- private static func removingModelLine(
1266
- from section: InsightsSection?,
1267
- when remove: Bool) -> InsightsSection?
1268
- {
1269
- guard remove, let section else { return section }
1270
- return InsightsSection(
1271
- title: section.title,
1272
- todayLine: section.todayLine,
1273
- todayDetail: section.todayDetail,
1274
- forecastLine: section.forecastLine,
1275
- blockLine: section.blockLine,
1276
- blockDetail: section.blockDetail,
1277
- modelLine: nil,
1278
- projectLine: section.projectLine,
1279
- projectDetail: section.projectDetail,
1280
- anomalyLine: section.anomalyLine,
1281
- anomalyDetail: section.anomalyDetail,
1282
- reliabilityLine: section.reliabilityLine,
1283
- reliabilityDetail: section.reliabilityDetail,
1284
- routingLine: section.routingLine,
1285
- routingDetail: section.routingDetail,
1286
- updatedLine: section.updatedLine,
1287
- errorLine: section.errorLine)
1288
- }
1289
-
1290
- private static func email(
1291
- for provider: UsageProvider,
1292
- snapshot: UsageSnapshot?,
1293
- account: AccountInfo,
1294
- metadata: ProviderMetadata) -> String
1295
- {
1296
- if let email = snapshot?.accountEmail(for: provider), !email.isEmpty { return email }
1297
- if metadata.usesAccountFallback,
1298
- let email = account.email, !email.isEmpty
1299
- {
1300
- return email
1301
- }
1302
- return ""
1303
- }
1304
-
1305
- private static func plan(
1306
- for provider: UsageProvider,
1307
- snapshot: UsageSnapshot?,
1308
- account: AccountInfo,
1309
- metadata: ProviderMetadata) -> String?
1310
- {
1311
- if let plan = snapshot?.loginMethod(for: provider), !plan.isEmpty {
1312
- return self.planDisplay(plan)
1313
- }
1314
- if metadata.usesAccountFallback,
1315
- let plan = account.plan, !plan.isEmpty
1316
- {
1317
- return Self.planDisplay(plan)
1318
- }
1319
- return nil
1320
- }
1321
-
1322
- private static func planDisplay(_ text: String) -> String {
1323
- let cleaned = UsageFormatter.cleanPlanName(text)
1324
- return cleaned.isEmpty ? text : cleaned
1325
- }
1326
-
1327
- private static func subtitle(
1328
- snapshot: UsageSnapshot?,
1329
- isRefreshing: Bool,
1330
- lastError: String?) -> (text: String, style: SubtitleStyle)
1331
- {
1332
- if let lastError, !lastError.isEmpty {
1333
- return (lastError.trimmingCharacters(in: .whitespacesAndNewlines), .error)
1334
- }
1335
-
1336
- if isRefreshing, snapshot == nil {
1337
- return ("Refreshing...", .loading)
1338
- }
1339
-
1340
- if let updated = snapshot?.updatedAt {
1341
- return (UsageFormatter.updatedString(from: updated), .info)
1342
- }
1343
-
1344
- return ("Not fetched yet", .info)
1345
- }
1346
-
1347
- private static func metrics(input: Input) -> [Metric] {
1348
- guard let snapshot = input.snapshot else { return [] }
1349
- var metrics: [Metric] = []
1350
- let percentStyle: PercentStyle = input.usageBarsShowUsed ? .used : .left
1351
- let zaiUsage = input.provider == .zai ? snapshot.zaiUsage : nil
1352
- let zaiTokenDetail = Self.zaiLimitDetailText(limit: zaiUsage?.tokenLimit)
1353
- let zaiTimeDetail = Self.zaiLimitDetailText(limit: zaiUsage?.timeLimit)
1354
- metrics.append(Metric(
1355
- id: "primary",
1356
- title: input.metadata.sessionLabel,
1357
- percent: Self.clamped(
1358
- input.usageBarsShowUsed ? snapshot.primary.usedPercent : snapshot.primary.remainingPercent),
1359
- percentStyle: percentStyle,
1360
- resetText: Self.resetText(for: snapshot.primary, prefersCountdown: true),
1361
- detailText: input.provider == .zai ? zaiTokenDetail : nil))
1362
- if let weekly = snapshot.secondary {
1363
- let paceText = UsagePaceText.weekly(provider: input.provider, window: weekly, now: input.now)
1364
- metrics.append(Metric(
1365
- id: "secondary",
1366
- title: input.metadata.weeklyLabel,
1367
- percent: Self.clamped(input.usageBarsShowUsed ? weekly.usedPercent : weekly.remainingPercent),
1368
- percentStyle: percentStyle,
1369
- resetText: Self.resetText(for: weekly, prefersCountdown: true),
1370
- detailText: input.provider == .zai ? zaiTimeDetail : paceText))
1371
- }
1372
- if input.metadata.supportsOpus, let opus = snapshot.tertiary {
1373
- metrics.append(Metric(
1374
- id: "tertiary",
1375
- title: input.metadata.opusLabel ?? "Sonnet",
1376
- percent: Self.clamped(input.usageBarsShowUsed ? opus.usedPercent : opus.remainingPercent),
1377
- percentStyle: percentStyle,
1378
- resetText: Self.resetText(for: opus, prefersCountdown: true),
1379
- detailText: nil))
1380
- }
1381
-
1382
- if input.provider == .codex, let remaining = input.dashboard?.codeReviewRemainingPercent {
1383
- let percent = input.usageBarsShowUsed ? (100 - remaining) : remaining
1384
- metrics.append(Metric(
1385
- id: "code-review",
1386
- title: "Code review",
1387
- percent: Self.clamped(percent),
1388
- percentStyle: percentStyle,
1389
- resetText: nil,
1390
- detailText: nil))
1391
- }
1392
- return metrics
1393
- }
1394
-
1395
- private static func zaiLimitDetailText(limit: ZaiLimitEntry?) -> String? {
1396
- guard let limit else { return nil }
1397
- let currentStr = UsageFormatter.tokenCountString(limit.currentValue)
1398
- let usageStr = UsageFormatter.tokenCountString(limit.usage)
1399
- let remainingStr = UsageFormatter.tokenCountString(limit.remaining)
1400
- return "\(currentStr) / \(usageStr) (\(remainingStr) remaining)"
1401
- }
1402
-
1403
- private static func creditsLine(
1404
- metadata: ProviderMetadata,
1405
- credits: CreditsSnapshot?,
1406
- error: String?) -> String?
1407
- {
1408
- guard metadata.supportsCredits else { return nil }
1409
- if let credits {
1410
- return UsageFormatter.creditsString(from: credits.remaining)
1411
- }
1412
- if let error, !error.isEmpty {
1413
- return error.trimmingCharacters(in: .whitespacesAndNewlines)
1414
- }
1415
- return metadata.creditsHint
1416
- }
1417
-
1418
- private static func dashboardHint(provider: UsageProvider, error: String?) -> String? {
1419
- guard provider == .codex else { return nil }
1420
- guard let error, !error.isEmpty else { return nil }
1421
- return error
1422
- }
1423
-
1424
- private static func tokenUsageSection(
1425
- provider: UsageProvider,
1426
- enabled: Bool,
1427
- snapshot: CostUsageTokenSnapshot?,
1428
- error: String?) -> TokenUsageSection?
1429
- {
1430
- guard provider == .codex || provider == .claude else { return nil }
1431
- guard enabled else { return nil }
1432
- guard let snapshot else { return nil }
1433
-
1434
- let sessionCostValue = snapshot.sessionCostUSD
1435
- let sessionCost = sessionCostValue.map { UsageFormatter.usdString($0) } ?? "—"
1436
- let sessionTokens = snapshot.sessionTokens.map { UsageFormatter.tokenCountString($0) }
1437
- let sessionLine: String = {
1438
- if let sessionTokens {
1439
- return "Today: \(sessionCost) · \(sessionTokens) tokens"
1440
- }
1441
- return "Today: \(sessionCost)"
1442
- }()
1443
- let sessionDetailLine: String? = {
1444
- guard let cost = sessionCostValue,
1445
- let tokens = snapshot.sessionTokens,
1446
- let per1K = UsageFormatter.usdPer1KTokensString(costUSD: cost, tokenCount: tokens)
1447
- else {
1448
- return nil
1449
- }
1450
- return "Today efficiency: \(per1K)"
1451
- }()
1452
-
1453
- let fallbackCost = snapshot.daily.compactMap(\.costUSD).reduce(0, +)
1454
- let monthCostValue = snapshot.last30DaysCostUSD ?? (fallbackCost > 0 ? fallbackCost : nil)
1455
- let monthCost = monthCostValue.map { UsageFormatter.usdString($0) } ?? "—"
1456
- let fallbackTokens = snapshot.daily.compactMap(\.totalTokens).reduce(0, +)
1457
- let monthTokensValue = snapshot.last30DaysTokens ?? (fallbackTokens > 0 ? fallbackTokens : nil)
1458
- let monthTokens = monthTokensValue.map { UsageFormatter.tokenCountString($0) }
1459
- let monthLine: String = {
1460
- if let monthTokens {
1461
- return "Last 30 days: \(monthCost) · \(monthTokens) tokens"
1462
- }
1463
- return "Last 30 days: \(monthCost)"
1464
- }()
1465
- let monthDetailLine: String? = {
1466
- var parts: [String] = []
1467
- if let cost = monthCostValue,
1468
- let tokens = monthTokensValue,
1469
- let per1K = UsageFormatter.usdPer1KTokensString(costUSD: cost, tokenCount: tokens)
1470
- {
1471
- parts.append(per1K)
1472
- }
1473
-
1474
- if let cost = monthCostValue, let tokens = monthTokensValue, tokens > 0 {
1475
- let days = Self.observedUsageDays(snapshot)
1476
- let avgCostPerDay = cost / Double(days)
1477
- let avgTokensPerDay = Int((Double(tokens) / Double(days)).rounded())
1478
- parts.append("Avg \(UsageFormatter.usdRateString(avgCostPerDay))/day")
1479
- parts.append("Avg \(UsageFormatter.tokenCountString(avgTokensPerDay)) tok/day")
1480
- }
1481
-
1482
- return parts.isEmpty ? nil : parts.joined(separator: " · ")
1483
- }()
1484
- let updatedLine = UsageFormatter.updatedString(from: snapshot.updatedAt)
1485
- let err = (error?.isEmpty ?? true) ? nil : error
1486
- let hintLine = err == nil ? "Token totals are estimates and may lag provider dashboards." : nil
1487
- return TokenUsageSection(
1488
- sessionLine: sessionLine,
1489
- sessionDetailLine: sessionDetailLine,
1490
- monthLine: monthLine,
1491
- monthDetailLine: monthDetailLine,
1492
- updatedLine: updatedLine,
1493
- hintLine: hintLine,
1494
- errorLine: err,
1495
- errorCopyText: (error?.isEmpty ?? true) ? nil : error)
1496
- }
1497
-
1498
- private static func observedUsageDays(_ snapshot: CostUsageTokenSnapshot) -> Int {
1499
- let nonEmptyDays = snapshot.daily.filter { entry in
1500
- (entry.totalTokens ?? 0) > 0 || (entry.costUSD ?? 0) > 0
1501
- }.count
1502
- if nonEmptyDays > 0 {
1503
- return min(30, nonEmptyDays)
1504
- }
1505
- if !snapshot.daily.isEmpty {
1506
- return min(30, snapshot.daily.count)
1507
- }
1508
- return 30
1509
- }
1510
-
1511
- private static func providerCostSection(
1512
- provider: UsageProvider,
1513
- cost: ProviderCostSnapshot?) -> ProviderCostSection?
1514
- {
1515
- guard provider == .claude else { return nil }
1516
- guard let cost else { return nil }
1517
- guard cost.limit > 0 else { return nil }
1518
-
1519
- let used = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode)
1520
- let limit = UsageFormatter.currencyString(cost.limit, currencyCode: cost.currencyCode)
1521
- let percentUsed = Self.clamped((cost.used / cost.limit) * 100)
1522
-
1523
- return ProviderCostSection(
1524
- title: "Extra usage",
1525
- percentUsed: percentUsed,
1526
- spendLine: "This month: \(used) / \(limit)")
1527
- }
1528
-
1529
- private static func insightsSection(input: Input) -> InsightsSection? {
1530
- let error = input.ledgerError?.trimmingCharacters(in: .whitespacesAndNewlines)
1531
- let daily = input.ledgerDaily?.provider == input.provider ? input.ledgerDaily : nil
1532
- let block = input.ledgerActiveBlock?.provider == input.provider ? input.ledgerActiveBlock : nil
1533
- let topModel = input.ledgerTopModel?.provider == input.provider ? input.ledgerTopModel : nil
1534
- let topProject = input.ledgerTopProject?.provider == input.provider ? input.ledgerTopProject : nil
1535
- let spendForecast = input.ledgerSpendForecast?.provider == input.provider ? input.ledgerSpendForecast : nil
1536
- let topProjectSpendForecast = input.ledgerTopProjectSpendForecast?.provider == input.provider
1537
- ? input.ledgerTopProjectSpendForecast
1538
- : nil
1539
- let anomaly = input.ledgerAnomaly?.provider == input.provider ? input.ledgerAnomaly : nil
1540
- let hasData = daily != nil || block != nil || topModel != nil || topProject != nil || spendForecast != nil || anomaly != nil
1541
- if !hasData && (error?.isEmpty ?? true) {
1542
- return nil
1543
- }
1544
-
1545
- var todayLine: String?
1546
- var todayDetail: String?
1547
- if let daily {
1548
- let tokens = UsageFormatter.tokenCountString(daily.totals.totalTokens)
1549
- todayLine = "Today: \(tokens) tokens"
1550
- var details: [String] = []
1551
- let input = UsageFormatter.tokenCountString(daily.totals.inputTokens)
1552
- let output = UsageFormatter.tokenCountString(daily.totals.outputTokens)
1553
- var flowParts = ["In \(input)", "Out \(output)"]
1554
- if daily.totals.cacheReadTokens > 0 {
1555
- let cacheRead = UsageFormatter.tokenCountString(daily.totals.cacheReadTokens)
1556
- flowParts.append("Cache read \(cacheRead)")
1557
- }
1558
- if daily.totals.cacheCreationTokens > 0 {
1559
- let cacheWrite = UsageFormatter.tokenCountString(daily.totals.cacheCreationTokens)
1560
- flowParts.append("Cache write \(cacheWrite)")
1561
- }
1562
- details.append(flowParts.joined(separator: " · "))
1563
- if let cost = daily.totals.costUSD {
1564
- var spendParts = ["Spend \(UsageFormatter.usdString(cost))"]
1565
- if let per1K = UsageFormatter.usdPer1KTokensString(
1566
- costUSD: cost,
1567
- tokenCount: daily.totals.totalTokens)
1568
- {
1569
- spendParts.append(per1K)
1570
- }
1571
- details.append(spendParts.joined(separator: " · "))
1572
- }
1573
- if !details.isEmpty {
1574
- todayDetail = details.joined(separator: "\n")
1575
- }
1576
- }
1577
-
1578
- var forecastLine: String?
1579
- if let spendForecast {
1580
- let projected = UsageFormatter.usdString(spendForecast.projected30DayCostUSD)
1581
- let observedDayLabel = spendForecast.observedDays == 1 ? "day" : "days"
1582
- var parts = ["Month-end forecast: \(projected)"]
1583
- if let p50 = spendForecast.projectedCostP50USD,
1584
- let p80 = spendForecast.projectedCostP80USD,
1585
- let p95 = spendForecast.projectedCostP95USD
1586
- {
1587
- let p50Text = UsageFormatter.usdString(p50)
1588
- let p80Text = UsageFormatter.usdString(p80)
1589
- let p95Text = UsageFormatter.usdString(p95)
1590
- parts.append("p50 \(p50Text) · p80 \(p80Text) · p95 \(p95Text)")
1591
- }
1592
- parts.append("\(spendForecast.observedDays) observed \(observedDayLabel)")
1593
- forecastLine = parts.joined(separator: " · ")
1594
- }
1595
-
1596
- var blockLine: String?
1597
- var blockDetail: String?
1598
- if let block, block.isActive {
1599
- let tokens = UsageFormatter.tokenCountString(block.totals.totalTokens)
1600
- blockLine = "Active block: \(tokens) tokens · \(block.entryCount) req"
1601
- var details: [String] = []
1602
- details.append("Ends \(UsageFormatter.resetCountdownDescription(from: block.end, now: input.now))")
1603
- if let rate = block.tokensPerMinute {
1604
- let rateText = UsageFormatter.tokenCountString(Int(rate.rounded()))
1605
- details.append("\(rateText) tok/min")
1606
- }
1607
- if let projected = block.projectedTotalTokens {
1608
- details.append("Proj \(UsageFormatter.tokenCountString(projected))")
1609
- }
1610
- let inputTokens = UsageFormatter.tokenCountString(block.totals.inputTokens)
1611
- let outputTokens = UsageFormatter.tokenCountString(block.totals.outputTokens)
1612
- details.append("In \(inputTokens) · Out \(outputTokens)")
1613
- if let cost = block.totals.costUSD {
1614
- var spendParts = ["Spend \(UsageFormatter.usdString(cost))"]
1615
- if let per1K = UsageFormatter.usdPer1KTokensString(
1616
- costUSD: cost,
1617
- tokenCount: block.totals.totalTokens)
1618
- {
1619
- spendParts.append(per1K)
1620
- }
1621
- if let perRequest = UsageFormatter.usdPerRequestString(
1622
- costUSD: cost,
1623
- requestCount: block.entryCount)
1624
- {
1625
- spendParts.append(perRequest)
1626
- }
1627
- if let burnPerHour = UsageFormatter.usdPerHourFromTokensString(
1628
- costUSD: cost,
1629
- tokenCount: block.totals.totalTokens,
1630
- tokensPerMinute: block.tokensPerMinute)
1631
- {
1632
- spendParts.append("Burn \(burnPerHour)")
1633
- }
1634
- details.append(spendParts.joined(separator: " · "))
1635
- }
1636
- blockDetail = details.joined(separator: "\n")
1637
- }
1638
-
1639
- var modelLine: String?
1640
- if let topModel {
1641
- let tokens = UsageFormatter.tokenCountString(topModel.totals.totalTokens)
1642
- let modelName = UsageFormatter.modelDisplayName(topModel.model)
1643
- var parts = ["Top model: \(modelName) · \(tokens) tokens · \(topModel.entryCount) req"]
1644
- if let context = UsageFormatter.modelContextLabel(for: topModel.model) {
1645
- parts.append(context)
1646
- }
1647
- if let cost = topModel.totals.costUSD {
1648
- parts.append(UsageFormatter.usdString(cost))
1649
- if let per1K = UsageFormatter.usdPer1KTokensString(
1650
- costUSD: cost,
1651
- tokenCount: topModel.totals.totalTokens)
1652
- {
1653
- parts.append(per1K)
1654
- }
1655
- }
1656
- modelLine = parts.joined(separator: " · ")
1657
- }
1658
-
1659
- var projectLine: String?
1660
- var projectDetail: String?
1661
- if let topProject {
1662
- let name = Self.insightsProjectDisplayName(topProject)
1663
- let tokens = UsageFormatter.tokenCountString(topProject.totals.totalTokens)
1664
- var parts = ["Top project: \(name) · \(tokens) tokens · \(topProject.entryCount) req"]
1665
- if let cost = topProject.totals.costUSD {
1666
- parts.append(UsageFormatter.usdString(cost))
1667
- if let per1K = UsageFormatter.usdPer1KTokensString(
1668
- costUSD: cost,
1669
- tokenCount: topProject.totals.totalTokens)
1670
- {
1671
- parts.append(per1K)
1672
- }
1673
- }
1674
- projectLine = parts.joined(separator: " · ")
1675
-
1676
- if let projectForecast = topProjectSpendForecast {
1677
- var detailParts = ["30d forecast \(UsageFormatter.usdString(projectForecast.projected30DayCostUSD))"]
1678
- if let budgetLimit = projectForecast.budgetLimitUSD {
1679
- detailParts.append("Budget \(UsageFormatter.usdString(budgetLimit))")
1680
- if let budgetETAInDays = projectForecast.budgetETAInDays {
1681
- detailParts.append(Self.budgetBreachETAText(days: budgetETAInDays, now: input.now))
1682
- } else if !projectForecast.budgetWillBreach {
1683
- detailParts.append("No breach at current pace")
1684
- }
1685
- }
1686
- projectDetail = detailParts.joined(separator: " · ")
1687
- }
1688
- }
1689
-
1690
- var anomalyLine: String?
1691
- var anomalyDetail: String?
1692
- if let anomaly, let primary = anomaly.primaryAnomaly {
1693
- anomalyLine = "Anomaly: \(primary.severity.label) \(primary.metric.label) spike"
1694
- var details = [Self.anomalyMetricDetail(primary, baselineDays: anomaly.baselineDays)]
1695
- if let secondary = anomaly.secondaryAnomaly(excluding: primary.metric) {
1696
- details.append(Self.anomalyMetricDetail(secondary, baselineDays: anomaly.baselineDays))
1697
- }
1698
- anomalyDetail = details.joined(separator: "\n")
1699
- }
1700
-
1701
- var reliabilityLine: String?
1702
- var reliabilityDetail: String?
1703
- if let reliability = input.ledgerReliability {
1704
- reliabilityLine = "Reliability: \(reliability.score)/100 · \(reliability.grade)"
1705
- reliabilityDetail = reliability.primarySignal ?? reliability.summary
1706
- }
1707
-
1708
- var routingLine: String?
1709
- var routingDetail: String?
1710
- if let routing = input.ledgerRouting {
1711
- let from = UsageFormatter.modelDisplayName(routing.fromModel)
1712
- let to = UsageFormatter.modelDisplayName(routing.toModel)
1713
- routingLine = "Routing advisor: shift \(routing.shiftPercent)% \(from) -> \(to)"
1714
- let confidenceText = "\(Int((routing.confidence * 100).rounded()))%"
1715
- routingDetail = "Estimated savings: \(UsageFormatter.usdString(routing.estimatedSavingsUSD)) · confidence \(confidenceText)"
1716
- }
1717
-
1718
- let updatedLine = input.ledgerUpdatedAt.map { UsageFormatter.updatedString(from: $0, now: input.now) }
1719
-
1720
- return InsightsSection(
1721
- title: "Insights",
1722
- todayLine: todayLine,
1723
- todayDetail: todayDetail,
1724
- forecastLine: forecastLine,
1725
- blockLine: blockLine,
1726
- blockDetail: blockDetail,
1727
- modelLine: modelLine,
1728
- projectLine: projectLine,
1729
- projectDetail: projectDetail,
1730
- anomalyLine: anomalyLine,
1731
- anomalyDetail: anomalyDetail,
1732
- reliabilityLine: reliabilityLine,
1733
- reliabilityDetail: reliabilityDetail,
1734
- routingLine: routingLine,
1735
- routingDetail: routingDetail,
1736
- updatedLine: updatedLine,
1737
- errorLine: (error?.isEmpty ?? true) ? nil : error)
1738
- }
1739
-
1740
- private static func anomalyMetricDetail(
1741
- _ anomaly: UsageLedgerAnomalySummary.MetricAnomaly,
1742
- baselineDays: Int) -> String
1743
- {
1744
- let percentText = "\(Int((anomaly.percentIncrease * 100).rounded()))%"
1745
- let baselineLabel = "\(baselineDays)d avg"
1746
- switch anomaly.metric {
1747
- case .tokens:
1748
- let todayTokens = UsageFormatter.tokenCountString(Int(anomaly.todayValue.rounded()))
1749
- let baselineTokens = UsageFormatter.tokenCountString(Int(anomaly.baselineAverage.rounded()))
1750
- return "Tokens \(todayTokens) today · +\(percentText) vs \(baselineLabel) \(baselineTokens)"
1751
- case .spend:
1752
- let todaySpend = UsageFormatter.usdString(anomaly.todayValue)
1753
- let baselineSpend = UsageFormatter.usdString(anomaly.baselineAverage)
1754
- return "Spend \(todaySpend) today · +\(percentText) vs \(baselineLabel) \(baselineSpend)"
1755
- }
1756
- }
1757
-
1758
- private static func insightsProjectDisplayName(_ summary: UsageLedgerProjectSummary) -> String {
1759
- let displayName = summary.displayProjectName
1760
- guard let annotation = self.projectIdentityAnnotation(
1761
- displayName: displayName,
1762
- projectID: summary.projectID,
1763
- confidence: summary.projectNameConfidence,
1764
- source: summary.projectNameSource)
1765
- else {
1766
- return displayName
1767
- }
1768
- return "\(displayName) [\(annotation)]"
1769
- }
1770
-
1771
- private static func projectIdentityAnnotation(
1772
- displayName: String,
1773
- projectID: String?,
1774
- confidence: UsageLedgerProjectNameConfidence?,
1775
- source: UsageLedgerProjectNameSource?) -> String?
1776
- {
1777
- let normalizedSource = source ?? .unknown
1778
- let normalizedConfidence = confidence ?? .none
1779
-
1780
- let shouldAnnotateSource = normalizedSource != .projectName && normalizedSource != .budgetOverride
1781
- let shouldAnnotateConfidence = normalizedConfidence != .high
1782
- let isUnknown = displayName == "Unknown project"
1783
- guard shouldAnnotateSource || shouldAnnotateConfidence || isUnknown else { return nil }
1784
-
1785
- var parts: [String] = []
1786
- if shouldAnnotateSource {
1787
- parts.append("source \(self.projectSourceLabel(normalizedSource))")
1788
- }
1789
- if shouldAnnotateConfidence {
1790
- parts.append("confidence \(self.projectConfidenceLabel(normalizedConfidence))")
1791
- }
1792
- if isUnknown, let fingerprint = self.projectIDFingerprint(projectID) {
1793
- parts.append("id \(fingerprint)")
1794
- }
1795
- return parts.isEmpty ? nil : parts.joined(separator: " · ")
1796
- }
1797
-
1798
- private static func projectSourceLabel(_ source: UsageLedgerProjectNameSource) -> String {
1799
- switch source {
1800
- case .projectName: "project name"
1801
- case .projectID: "project id"
1802
- case .inferredFromPath: "path-derived"
1803
- case .inferredFromName: "name-derived"
1804
- case .budgetOverride: "budget override"
1805
- case .unknown: "unknown"
1806
- }
1807
- }
1808
-
1809
- private static func projectConfidenceLabel(_ confidence: UsageLedgerProjectNameConfidence) -> String {
1810
- switch confidence {
1811
- case .high: "high"
1812
- case .medium: "medium"
1813
- case .low: "low"
1814
- case .none: "none"
1815
- }
1816
- }
1817
-
1818
- private static func projectIDFingerprint(_ projectID: String?) -> String? {
1819
- guard let projectID = projectID?.trimmingCharacters(in: .whitespacesAndNewlines), !projectID.isEmpty else {
1820
- return nil
1821
- }
1822
-
1823
- var hash: UInt64 = 0xcbf29ce484222325
1824
- for byte in projectID.lowercased().utf8 {
1825
- hash ^= UInt64(byte)
1826
- hash = hash &* 0x100000001b3
1827
- }
1828
- return String(format: "%08llx", hash)
1829
- }
1830
-
1831
- private static func budgetBreachETAText(days: Double, now: Date) -> String {
1832
- guard days.isFinite else { return "Breach ETA unavailable" }
1833
- if days <= 0 { return "Breach now" }
1834
- let etaDate = now.addingTimeInterval(days * 24 * 60 * 60)
1835
- let countdown = UsageFormatter.resetCountdownDescription(from: etaDate, now: now)
1836
- if countdown == "now" { return "Breach now" }
1837
- return "Breach \(countdown)"
1838
- }
1839
-
1840
- private static func clamped(_ value: Double) -> Double {
1841
- min(100, max(0, value))
1842
- }
1843
-
1844
- private static func progressColor(for provider: UsageProvider) -> Color {
1845
- let color = ProviderDescriptorRegistry.descriptor(for: provider).branding.color
1846
- return Color(red: color.red, green: color.green, blue: color.blue)
1847
- }
1848
-
1849
- private static func resetText(for window: RateWindow, prefersCountdown: Bool) -> String? {
1850
- if let date = window.resetsAt {
1851
- if prefersCountdown {
1852
- return "Resets \(UsageFormatter.resetCountdownDescription(from: date))"
1853
- }
1854
- return "Resets \(UsageFormatter.resetDescription(from: date))"
1855
- }
1856
-
1857
- if let desc = window.resetDescription, !desc.isEmpty {
1858
- return desc
1859
- }
1860
- return nil
1861
- }
1862
- }
1863
-
1864
- // MARK: - Copy-on-click overlay
1865
-
1866
- private struct ClickToCopyOverlay: NSViewRepresentable {
1867
- let copyText: String
1868
-
1869
- func makeNSView(context: Context) -> ClickToCopyView {
1870
- ClickToCopyView(copyText: self.copyText)
1871
- }
1872
-
1873
- func updateNSView(_ nsView: ClickToCopyView, context: Context) {
1874
- nsView.copyText = self.copyText
1875
- }
1876
- }
1877
-
1878
- private final class ClickToCopyView: NSView {
1879
- var copyText: String
1880
-
1881
- init(copyText: String) {
1882
- self.copyText = copyText
1883
- super.init(frame: .zero)
1884
- self.wantsLayer = false
1885
- }
1886
-
1887
- @available(*, unavailable)
1888
- required init?(coder: NSCoder) {
1889
- fatalError("init(coder:) has not been implemented")
1890
- }
1891
-
1892
- override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true }
1893
-
1894
- override func mouseDown(with event: NSEvent) {
1895
- _ = event
1896
- let pb = NSPasteboard.general
1897
- pb.clearContents()
1898
- pb.setString(self.copyText, forType: .string)
1899
- }
1900
- }