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