@sriinnu/runic 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (584) hide show
  1. package/.swiftformat +50 -0
  2. package/.swiftlint.yml +160 -0
  3. package/CHANGELOG.md +307 -0
  4. package/CONTRIBUTING.md +456 -0
  5. package/Icon.icns +0 -0
  6. package/Icon.icon/Assets/runic.png +0 -0
  7. package/Icon.icon/icon.json +36 -0
  8. package/LICENSE +21 -0
  9. package/Package.resolved +51 -0
  10. package/Package.swift +136 -0
  11. package/Packages/Helix/LICENSE +21 -0
  12. package/Packages/Helix/Package.swift +29 -0
  13. package/Packages/Helix/README.md +976 -0
  14. package/Packages/Helix/Sources/Helix/Binding.swift +111 -0
  15. package/Packages/Helix/Sources/Helix/CommandParser.swift +160 -0
  16. package/Packages/Helix/Sources/Helix/CommandSignature.swift +117 -0
  17. package/Packages/Helix/Sources/Helix/ExpressibleFromArgument.swift +72 -0
  18. package/Packages/Helix/Sources/Helix/HelpGenerator.swift +175 -0
  19. package/Packages/Helix/Sources/Helix/NameSpecification.swift +98 -0
  20. package/Packages/Helix/Sources/Helix/ParsableCommand.swift +204 -0
  21. package/Packages/Helix/Sources/Helix/Platform/DarwinPlatformContext.swift +44 -0
  22. package/Packages/Helix/Sources/Helix/Platform/LinuxPlatformContext.swift +34 -0
  23. package/Packages/Helix/Sources/Helix/Platform/PlatformContext+Default.swift +91 -0
  24. package/Packages/Helix/Sources/Helix/Platform/PlatformContext.swift +94 -0
  25. package/Packages/Helix/Sources/Helix/Platform/PlatformPath.swift +167 -0
  26. package/Packages/Helix/Sources/Helix/Platform/Stdio.swift +99 -0
  27. package/Packages/Helix/Sources/Helix/Platform/WebPlatformContext.swift +146 -0
  28. package/Packages/Helix/Sources/Helix/Platform/WindowsPlatformContext.swift +97 -0
  29. package/Packages/Helix/Sources/Helix/Program.swift +204 -0
  30. package/Packages/Helix/Sources/Helix/PropertyWrappers.swift +367 -0
  31. package/Packages/Silo/LICENSE +21 -0
  32. package/Packages/Silo/Package.swift +34 -0
  33. package/Packages/Silo/README.md +466 -0
  34. package/Packages/Silo/Sources/Silo/BinaryCookiesReader.swift +169 -0
  35. package/Packages/Silo/Sources/Silo/BrowserCookieAnalytics.swift +365 -0
  36. package/Packages/Silo/Sources/Silo/BrowserCookieBackends.swift +594 -0
  37. package/Packages/Silo/Sources/Silo/BrowserCookieClient.swift +326 -0
  38. package/Packages/Silo/Sources/Silo/BrowserCookieExport.swift +154 -0
  39. package/Packages/Silo/Sources/Silo/BrowserCookieImport.swift +119 -0
  40. package/Packages/Silo/Sources/Silo/BrowserCookieMockStore.swift +121 -0
  41. package/Packages/Silo/Sources/Silo/BrowserCookieModels.swift +391 -0
  42. package/Packages/Silo/Sources/Silo/ChromiumCookieReader.swift +240 -0
  43. package/Packages/Silo/Sources/Silo/CryptoSupport.swift +86 -0
  44. package/Packages/Silo/Sources/Silo/FirefoxCookieReader.swift +172 -0
  45. package/Packages/Silo/Sources/Silo/LinuxChromiumCookieReader.swift +172 -0
  46. package/Packages/Silo/Sources/Silo/LinuxSecretService.swift +105 -0
  47. package/Packages/Silo/Sources/Silo/MacOSChromiumCookieReader.swift +191 -0
  48. package/Packages/Silo/Sources/Silo/SQLiteSnapshot.swift +41 -0
  49. package/Packages/Silo/Sources/Silo/WindowsChromiumCookieReader.swift +110 -0
  50. package/README.md +109 -0
  51. package/RuniciOS/App/RuniciOSApp.swift +105 -0
  52. package/RuniciOS/Core/iOSUsageStore.swift +192 -0
  53. package/RuniciOS/Views/ProviderDetailView.swift +312 -0
  54. package/RuniciOS/Views/ProviderListView.swift +228 -0
  55. package/RuniciOS/Widgets/LargeWidget.swift +367 -0
  56. package/RuniciOS/Widgets/LiveActivity.swift +342 -0
  57. package/RuniciOS/Widgets/LockScreenWidget.swift +273 -0
  58. package/RuniciOS/Widgets/MediumWidget.swift +370 -0
  59. package/RuniciOS/Widgets/RunicWidget.swift +274 -0
  60. package/RuniciOS/Widgets/SmallWidget.swift +281 -0
  61. package/RuniciOS/Widgets/WidgetModels.swift +285 -0
  62. package/RuniciOS/Widgets/WidgetTimeline.swift +270 -0
  63. package/Scripts/CREATION-SUMMARY.md +367 -0
  64. package/Scripts/DIRECTORY-STRUCTURE.md +358 -0
  65. package/Scripts/GETTING-STARTED-CHECKLIST.md +364 -0
  66. package/Scripts/INDEX.md +365 -0
  67. package/Scripts/QUICK-REFERENCE.md +164 -0
  68. package/Scripts/README-SCRIPTS.md +926 -0
  69. package/Scripts/build-android.sh +526 -0
  70. package/Scripts/build-ios.sh +529 -0
  71. package/Scripts/build-macos.sh +603 -0
  72. package/Scripts/build-windows.sh +504 -0
  73. package/Scripts/build_icon.sh +49 -0
  74. package/Scripts/changelog-to-html.sh +89 -0
  75. package/Scripts/check-release-assets.sh +10 -0
  76. package/Scripts/ci-config.yml +474 -0
  77. package/Scripts/compile_and_run.sh +219 -0
  78. package/Scripts/compile_and_run_adhoc.sh +4 -0
  79. package/Scripts/docs-list.mjs +119 -0
  80. package/Scripts/make_appcast.sh +58 -0
  81. package/Scripts/package_app.sh +409 -0
  82. package/Scripts/release-lib.sh +214 -0
  83. package/Scripts/release.sh +76 -0
  84. package/Scripts/setup-all.sh +387 -0
  85. package/Scripts/setup-ios.sh +404 -0
  86. package/Scripts/setup-react-native.sh +523 -0
  87. package/Scripts/sign-and-notarize.sh +122 -0
  88. package/Scripts/test_live_update.sh +38 -0
  89. package/Scripts/validate_changelog.sh +23 -0
  90. package/Scripts/verify_appcast.sh +92 -0
  91. package/Sources/Runic/About.swift +93 -0
  92. package/Sources/Runic/AppNotifications.swift +115 -0
  93. package/Sources/Runic/Controllers/StatusItemController+Actions.swift +657 -0
  94. package/Sources/Runic/Controllers/StatusItemController+Animation.swift +518 -0
  95. package/Sources/Runic/Controllers/StatusItemController+ChartSubmenus.swift +810 -0
  96. package/Sources/Runic/Controllers/StatusItemController+CustomProviders.swift +87 -0
  97. package/Sources/Runic/Controllers/StatusItemController+Menu.swift +1379 -0
  98. package/Sources/Runic/Controllers/StatusItemController+MenuCards.swift +337 -0
  99. package/Sources/Runic/Controllers/StatusItemController+MenuHelpers.swift +395 -0
  100. package/Sources/Runic/Controllers/StatusItemController.swift +360 -0
  101. package/Sources/Runic/Core/BudgetNotificationManager.swift +80 -0
  102. package/Sources/Runic/Core/Login/ClaudeLoginRunner.swift +103 -0
  103. package/Sources/Runic/Core/Login/CodexLoginRunner.swift +154 -0
  104. package/Sources/Runic/Core/Login/CursorLoginRunner.swift +206 -0
  105. package/Sources/Runic/Core/Login/GeminiLoginRunner.swift +95 -0
  106. package/Sources/Runic/Core/Rendering/IconRenderer.swift +1216 -0
  107. package/Sources/Runic/Core/Rendering/IconView.swift +120 -0
  108. package/Sources/Runic/Core/Rendering/ProviderBrandIcon.swift +71 -0
  109. package/Sources/Runic/Core/RunicAnimation.swift +53 -0
  110. package/Sources/Runic/Core/RunicColors.swift +73 -0
  111. package/Sources/Runic/Core/RunicCornerRadius.swift +15 -0
  112. package/Sources/Runic/Core/RunicSpacing.swift +68 -0
  113. package/Sources/Runic/Core/Stores/AuggieTokenStore.swift +143 -0
  114. package/Sources/Runic/Core/Stores/AzureOpenAITokenStore.swift +143 -0
  115. package/Sources/Runic/Core/Stores/CerebrasTokenStore.swift +143 -0
  116. package/Sources/Runic/Core/Stores/CohereTokenStore.swift +143 -0
  117. package/Sources/Runic/Core/Stores/CopilotTokenStore.swift +143 -0
  118. package/Sources/Runic/Core/Stores/CustomProviderStore.swift +158 -0
  119. package/Sources/Runic/Core/Stores/DeepSeekTokenStore.swift +143 -0
  120. package/Sources/Runic/Core/Stores/FireworksTokenStore.swift +143 -0
  121. package/Sources/Runic/Core/Stores/GroqTokenStore.swift +143 -0
  122. package/Sources/Runic/Core/Stores/KimiTokenStore.swift +143 -0
  123. package/Sources/Runic/Core/Stores/MiniMaxCookieHeaderStore.swift +143 -0
  124. package/Sources/Runic/Core/Stores/MiniMaxGroupIDStore.swift +143 -0
  125. package/Sources/Runic/Core/Stores/MiniMaxTokenStore.swift +143 -0
  126. package/Sources/Runic/Core/Stores/MistralTokenStore.swift +143 -0
  127. package/Sources/Runic/Core/Stores/OpenRouterTokenStore.swift +143 -0
  128. package/Sources/Runic/Core/Stores/PerplexityTokenStore.swift +143 -0
  129. package/Sources/Runic/Core/Stores/ProviderToggleStore.swift +30 -0
  130. package/Sources/Runic/Core/Stores/QwenTokenStore.swift +143 -0
  131. package/Sources/Runic/Core/Stores/SambaNovaTokenStore.swift +143 -0
  132. package/Sources/Runic/Core/Stores/SettingsStore.swift +1702 -0
  133. package/Sources/Runic/Core/Stores/TogetherTokenStore.swift +143 -0
  134. package/Sources/Runic/Core/Stores/UsageStore+Accessors.swift +20 -0
  135. package/Sources/Runic/Core/Stores/UsageStore+Status.swift +189 -0
  136. package/Sources/Runic/Core/Stores/UsageStore+TokenCost.swift +33 -0
  137. package/Sources/Runic/Core/Stores/UsageStore+WidgetSnapshot.swift +67 -0
  138. package/Sources/Runic/Core/Stores/UsageStore.swift +3564 -0
  139. package/Sources/Runic/Core/Stores/XAITokenStore.swift +143 -0
  140. package/Sources/Runic/Core/Stores/ZaiTokenStore.swift +143 -0
  141. package/Sources/Runic/Core/UsageExporter.swift +193 -0
  142. package/Sources/Runic/Date+RelativeDescription.swift +21 -0
  143. package/Sources/Runic/HiddenWindowView.swift +32 -0
  144. package/Sources/Runic/InstallOrigin.swift +9 -0
  145. package/Sources/Runic/KeyboardShortcuts+Names.swift +6 -0
  146. package/Sources/Runic/MouseLocationReader.swift +65 -0
  147. package/Sources/Runic/Notifications+Runic.swift +6 -0
  148. package/Sources/Runic/OpenAICreditsPurchaseWindowController.swift +503 -0
  149. package/Sources/Runic/ProviderRegistry.swift +100 -0
  150. package/Sources/Runic/ProviderSwitcherButtons.swift +267 -0
  151. package/Sources/Runic/Providers/Antigravity/AntigravityLoginFlow.swift +11 -0
  152. package/Sources/Runic/Providers/Antigravity/AntigravityProviderImplementation.swift +14 -0
  153. package/Sources/Runic/Providers/Auggie/AuggieProviderImplementation.swift +24 -0
  154. package/Sources/Runic/Providers/Azure/AzureProviderImplementation.swift +52 -0
  155. package/Sources/Runic/Providers/Bedrock/BedrockProviderImplementation.swift +42 -0
  156. package/Sources/Runic/Providers/Cerebras/CerebrasProviderImplementation.swift +24 -0
  157. package/Sources/Runic/Providers/Claude/ClaudeLoginFlow.swift +26 -0
  158. package/Sources/Runic/Providers/Claude/ClaudeProviderImplementation.swift +14 -0
  159. package/Sources/Runic/Providers/Codex/CodexLoginFlow.swift +18 -0
  160. package/Sources/Runic/Providers/Codex/CodexProviderImplementation.swift +15 -0
  161. package/Sources/Runic/Providers/Cohere/CohereProviderImplementation.swift +24 -0
  162. package/Sources/Runic/Providers/Copilot/CopilotLoginFlow.swift +193 -0
  163. package/Sources/Runic/Providers/Copilot/CopilotProviderImplementation.swift +110 -0
  164. package/Sources/Runic/Providers/Cursor/CursorLoginFlow.swift +28 -0
  165. package/Sources/Runic/Providers/Cursor/CursorProviderImplementation.swift +15 -0
  166. package/Sources/Runic/Providers/DeepSeek/DeepSeekProviderImplementation.swift +24 -0
  167. package/Sources/Runic/Providers/Factory/FactoryLoginFlow.swift +12 -0
  168. package/Sources/Runic/Providers/Factory/FactoryProviderImplementation.swift +15 -0
  169. package/Sources/Runic/Providers/Fireworks/FireworksProviderImplementation.swift +24 -0
  170. package/Sources/Runic/Providers/Gemini/GeminiLoginFlow.swift +20 -0
  171. package/Sources/Runic/Providers/Gemini/GeminiProviderImplementation.swift +15 -0
  172. package/Sources/Runic/Providers/Groq/GroqProviderImplementation.swift +24 -0
  173. package/Sources/Runic/Providers/Kimi/KimiProviderImplementation.swift +24 -0
  174. package/Sources/Runic/Providers/MiniMax/MiniMaxProviderImplementation.swift +32 -0
  175. package/Sources/Runic/Providers/Mistral/MistralProviderImplementation.swift +24 -0
  176. package/Sources/Runic/Providers/OpenRouter/OpenRouterProviderImplementation.swift +24 -0
  177. package/Sources/Runic/Providers/Perplexity/PerplexityProviderImplementation.swift +24 -0
  178. package/Sources/Runic/Providers/SambaNova/SambaNovaProviderImplementation.swift +24 -0
  179. package/Sources/Runic/Providers/Shared/ProviderCatalog.swift +15 -0
  180. package/Sources/Runic/Providers/Shared/ProviderImplementation.swift +48 -0
  181. package/Sources/Runic/Providers/Shared/ProviderImplementationRegistry.swift +66 -0
  182. package/Sources/Runic/Providers/Shared/ProviderLoginFlow.swift +12 -0
  183. package/Sources/Runic/Providers/Shared/ProviderSettingsDescriptors.swift +99 -0
  184. package/Sources/Runic/Providers/Shared/SystemSettingsLinks.swift +18 -0
  185. package/Sources/Runic/Providers/Together/TogetherProviderImplementation.swift +24 -0
  186. package/Sources/Runic/Providers/XAI/XAIProviderImplementation.swift +24 -0
  187. package/Sources/Runic/Providers/Zai/ZaiProviderImplementation.swift +24 -0
  188. package/Sources/Runic/Resources/ProviderIcon-antigravity.svg +2 -0
  189. package/Sources/Runic/Resources/ProviderIcon-auggie.svg +3 -0
  190. package/Sources/Runic/Resources/ProviderIcon-azure.svg +5 -0
  191. package/Sources/Runic/Resources/ProviderIcon-bedrock.svg +7 -0
  192. package/Sources/Runic/Resources/ProviderIcon-cerebras.svg +3 -0
  193. package/Sources/Runic/Resources/ProviderIcon-claude.svg +1 -0
  194. package/Sources/Runic/Resources/ProviderIcon-codex.svg +1 -0
  195. package/Sources/Runic/Resources/ProviderIcon-cohere.svg +3 -0
  196. package/Sources/Runic/Resources/ProviderIcon-copilot.svg +1 -0
  197. package/Sources/Runic/Resources/ProviderIcon-cursor.svg +3 -0
  198. package/Sources/Runic/Resources/ProviderIcon-deepseek.svg +4 -0
  199. package/Sources/Runic/Resources/ProviderIcon-factory.svg +3 -0
  200. package/Sources/Runic/Resources/ProviderIcon-fireworks.svg +3 -0
  201. package/Sources/Runic/Resources/ProviderIcon-gemini.svg +1 -0
  202. package/Sources/Runic/Resources/ProviderIcon-groq.svg +1 -0
  203. package/Sources/Runic/Resources/ProviderIcon-kimi.svg +4 -0
  204. package/Sources/Runic/Resources/ProviderIcon-minimax.svg +1 -0
  205. package/Sources/Runic/Resources/ProviderIcon-mistral.svg +5 -0
  206. package/Sources/Runic/Resources/ProviderIcon-openrouter.svg +7 -0
  207. package/Sources/Runic/Resources/ProviderIcon-perplexity.svg +4 -0
  208. package/Sources/Runic/Resources/ProviderIcon-qwen.svg +9 -0
  209. package/Sources/Runic/Resources/ProviderIcon-sambanova.svg +4 -0
  210. package/Sources/Runic/Resources/ProviderIcon-together.svg +5 -0
  211. package/Sources/Runic/Resources/ProviderIcon-vertexai.svg +17 -0
  212. package/Sources/Runic/Resources/ProviderIcon-xai.svg +3 -0
  213. package/Sources/Runic/Resources/ProviderIcon-zai.svg +5 -0
  214. package/Sources/Runic/Resources/RunicMenubarIcon.svg +7 -0
  215. package/Sources/Runic/RunicApp.swift +357 -0
  216. package/Sources/Runic/SessionQuotaNotifications.swift +58 -0
  217. package/Sources/Runic/UsagePaceText.swift +52 -0
  218. package/Sources/Runic/UsageProgressBar.swift +224 -0
  219. package/Sources/Runic/Utilities/DisplayLink.swift +96 -0
  220. package/Sources/Runic/Utilities/LoadingPattern.swift +60 -0
  221. package/Sources/Runic/Utilities/PerformanceConstants.swift +85 -0
  222. package/Sources/Runic/Views/Menu/AlertsMenuView.swift +256 -0
  223. package/Sources/Runic/Views/Menu/ChartPanelStyle.swift +22 -0
  224. package/Sources/Runic/Views/Menu/CostComparativeChartMenuView.swift +275 -0
  225. package/Sources/Runic/Views/Menu/CostHistoryChartMenuView.swift +377 -0
  226. package/Sources/Runic/Views/Menu/CreditsHistoryChartMenuView.swift +312 -0
  227. package/Sources/Runic/Views/Menu/EfficiencyMetricsMenuView.swift +312 -0
  228. package/Sources/Runic/Views/Menu/HeroStatCardsMenuView.swift +228 -0
  229. package/Sources/Runic/Views/Menu/HourlyActivityChartMenuView.swift +228 -0
  230. package/Sources/Runic/Views/Menu/InlineUsageChartView.swift +162 -0
  231. package/Sources/Runic/Views/Menu/MenuCardEntranceModifier.swift +64 -0
  232. package/Sources/Runic/Views/Menu/MenuCardSidebarView.swift +268 -0
  233. package/Sources/Runic/Views/Menu/MenuCardSkeletonView.swift +104 -0
  234. package/Sources/Runic/Views/Menu/MenuCardView.swift +1900 -0
  235. package/Sources/Runic/Views/Menu/MenuContent.swift +150 -0
  236. package/Sources/Runic/Views/Menu/MenuDescriptor.swift +369 -0
  237. package/Sources/Runic/Views/Menu/MenuHighlightStyle.swift +42 -0
  238. package/Sources/Runic/Views/Menu/ModelBreakdownMenuView.swift +235 -0
  239. package/Sources/Runic/Views/Menu/OverviewMenuView.swift +141 -0
  240. package/Sources/Runic/Views/Menu/ProjectBreakdownMenuView.swift +209 -0
  241. package/Sources/Runic/Views/Menu/ProjectBudgetMenuView.swift +220 -0
  242. package/Sources/Runic/Views/Menu/ProviderTabBarView.swift +88 -0
  243. package/Sources/Runic/Views/Menu/SubscriptionUtilizationChartMenuView.swift +201 -0
  244. package/Sources/Runic/Views/Menu/UsageBreakdownChartMenuView.swift +398 -0
  245. package/Sources/Runic/Views/Menu/UsageHeatmapMenuView.swift +232 -0
  246. package/Sources/Runic/Views/Menu/UsageTimelineChartMenuView.swift +417 -0
  247. package/Sources/Runic/Views/Menu/UsageWindowComparisonChartMenuView.swift +190 -0
  248. package/Sources/Runic/Views/Menu/WeeklyActivityChartMenuView.swift +132 -0
  249. package/Sources/Runic/Views/Performance/PerformanceDashboardView.swift +618 -0
  250. package/Sources/Runic/Views/Preferences/LiquidComponents.swift +305 -0
  251. package/Sources/Runic/Views/Preferences/PreferencesAboutPane.swift +271 -0
  252. package/Sources/Runic/Views/Preferences/PreferencesAdvancedPane.swift +268 -0
  253. package/Sources/Runic/Views/Preferences/PreferencesAlertsPane.swift +422 -0
  254. package/Sources/Runic/Views/Preferences/PreferencesAnalyticsPane.swift +641 -0
  255. package/Sources/Runic/Views/Preferences/PreferencesBudgetsPane.swift +344 -0
  256. package/Sources/Runic/Views/Preferences/PreferencesComponents.swift +230 -0
  257. package/Sources/Runic/Views/Preferences/PreferencesCustomProvidersPane.swift +322 -0
  258. package/Sources/Runic/Views/Preferences/PreferencesDebugPane.swift +469 -0
  259. package/Sources/Runic/Views/Preferences/PreferencesGeneralPane.swift +231 -0
  260. package/Sources/Runic/Views/Preferences/PreferencesHelpPane.swift +50 -0
  261. package/Sources/Runic/Views/Preferences/PreferencesIntegrationsPane.swift +497 -0
  262. package/Sources/Runic/Views/Preferences/PreferencesPerformancePane.swift +288 -0
  263. package/Sources/Runic/Views/Preferences/PreferencesProvidersPane.swift +3435 -0
  264. package/Sources/Runic/Views/Preferences/PreferencesSelection.swift +25 -0
  265. package/Sources/Runic/Views/Preferences/PreferencesSyncPane.swift +59 -0
  266. package/Sources/Runic/Views/Preferences/PreferencesView.swift +206 -0
  267. package/Sources/Runic/Views/Rating/QualityRatingView.swift +238 -0
  268. package/Sources/Runic/Views/Team/TeamDashboardView.swift +371 -0
  269. package/Sources/Runic/Views/Team/TeamInviteSheet.swift +175 -0
  270. package/Sources/Runic/Views/Team/TeamManagementView.swift +761 -0
  271. package/Sources/Runic/Views/Team/TeamMemberRow.swift +181 -0
  272. package/Sources/RunicCLI/CLIEntry.swift +942 -0
  273. package/Sources/RunicCLI/CLIRenderer.swift +87 -0
  274. package/Sources/RunicCLI/Commands/EnhancedUsageCommand.swift +588 -0
  275. package/Sources/RunicCLI/Commands/ModelsCommand.swift +362 -0
  276. package/Sources/RunicCLI/Commands/ProjectsCommand.swift +439 -0
  277. package/Sources/RunicCLI/Commands/ResetCommand.swift +429 -0
  278. package/Sources/RunicCLI/Commands/UsageLedgerSourceFactory.swift +168 -0
  279. package/Sources/RunicClaudeWatchdog/main.swift +122 -0
  280. package/Sources/RunicClaudeWebProbe/main.swift +62 -0
  281. package/Sources/RunicCore/AlertRuleStore.swift +187 -0
  282. package/Sources/RunicCore/BrowserCookieImportOrder.swift +25 -0
  283. package/Sources/RunicCore/CopilotUsageModels.swift +84 -0
  284. package/Sources/RunicCore/CostUsageFetcher.swift +119 -0
  285. package/Sources/RunicCore/CostUsageModels.swift +479 -0
  286. package/Sources/RunicCore/CreditsModels.swift +50 -0
  287. package/Sources/RunicCore/CustomProviders/CustomProviderModels.swift +269 -0
  288. package/Sources/RunicCore/CustomProviders/GenericProviderFetcher.swift +622 -0
  289. package/Sources/RunicCore/CustomProviders/ProviderTemplates.swift +500 -0
  290. package/Sources/RunicCore/Host/PTY/TTYCommandRunner.swift +679 -0
  291. package/Sources/RunicCore/Host/Process/SubprocessRunner.swift +135 -0
  292. package/Sources/RunicCore/Integrations/GitHubIntegration.swift +173 -0
  293. package/Sources/RunicCore/Logging/JSONStderrLogHandler.swift +79 -0
  294. package/Sources/RunicCore/Logging/OSLogLogHandler.swift +86 -0
  295. package/Sources/RunicCore/Logging/RunicLog.swift +123 -0
  296. package/Sources/RunicCore/Models/EnhancedUsageModels.swift +371 -0
  297. package/Sources/RunicCore/OpenAIDashboardModels.swift +210 -0
  298. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardBrowserCookieImporter.swift +676 -0
  299. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardFetcher.swift +492 -0
  300. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardNavigationDelegate.swift +51 -0
  301. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardParser.swift +483 -0
  302. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardScrapeScript.swift +558 -0
  303. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardWebViewCache.swift +204 -0
  304. package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardWebsiteDataStore.swift +78 -0
  305. package/Sources/RunicCore/PathEnvironment.swift +482 -0
  306. package/Sources/RunicCore/Performance/PerformanceModels.swift +371 -0
  307. package/Sources/RunicCore/Performance/PerformanceStorageImpl.swift +409 -0
  308. package/Sources/RunicCore/Policy/UsagePolicyEngine.swift +178 -0
  309. package/Sources/RunicCore/ProjectBudgetStore.swift +99 -0
  310. package/Sources/RunicCore/ProviderCostSnapshot.swift +29 -0
  311. package/Sources/RunicCore/Providers/Antigravity/AntigravityProviderDescriptor.swift +66 -0
  312. package/Sources/RunicCore/Providers/Antigravity/AntigravityStatusProbe.swift +685 -0
  313. package/Sources/RunicCore/Providers/Auggie/AuggieProviderDescriptor.swift +83 -0
  314. package/Sources/RunicCore/Providers/Auggie/AuggieUsageFetcher.swift +142 -0
  315. package/Sources/RunicCore/Providers/Azure/AzureProviderDescriptor.swift +141 -0
  316. package/Sources/RunicCore/Providers/Azure/AzureUsageFetcher.swift +186 -0
  317. package/Sources/RunicCore/Providers/Bedrock/BedrockProviderDescriptor.swift +126 -0
  318. package/Sources/RunicCore/Providers/Bedrock/BedrockUsageFetcher.swift +168 -0
  319. package/Sources/RunicCore/Providers/Cerebras/CerebrasProviderDescriptor.swift +84 -0
  320. package/Sources/RunicCore/Providers/Cerebras/CerebrasUsageFetcher.swift +75 -0
  321. package/Sources/RunicCore/Providers/Claude/ClaudeCLISession.swift +332 -0
  322. package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift +180 -0
  323. package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthUsageFetcher.swift +137 -0
  324. package/Sources/RunicCore/Providers/Claude/ClaudeProviderDescriptor.swift +207 -0
  325. package/Sources/RunicCore/Providers/Claude/ClaudeStatusProbe.swift +654 -0
  326. package/Sources/RunicCore/Providers/Claude/ClaudeUsageDataSource.swift +28 -0
  327. package/Sources/RunicCore/Providers/Claude/ClaudeUsageFetcher.swift +480 -0
  328. package/Sources/RunicCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift +780 -0
  329. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthCredentials.swift +146 -0
  330. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift +269 -0
  331. package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexTokenRefresher.swift +100 -0
  332. package/Sources/RunicCore/Providers/Codex/CodexProviderDescriptor.swift +243 -0
  333. package/Sources/RunicCore/Providers/Codex/CodexStatusProbe.swift +125 -0
  334. package/Sources/RunicCore/Providers/Codex/CodexUsageDataSource.swift +24 -0
  335. package/Sources/RunicCore/Providers/Codex/CodexWebDashboardStrategy.swift +181 -0
  336. package/Sources/RunicCore/Providers/Cohere/CohereProviderDescriptor.swift +83 -0
  337. package/Sources/RunicCore/Providers/Cohere/CohereUsageFetcher.swift +85 -0
  338. package/Sources/RunicCore/Providers/Copilot/CopilotDeviceFlow.swift +123 -0
  339. package/Sources/RunicCore/Providers/Copilot/CopilotGitHubCLITokenReader.swift +114 -0
  340. package/Sources/RunicCore/Providers/Copilot/CopilotProviderDescriptor.swift +100 -0
  341. package/Sources/RunicCore/Providers/Copilot/CopilotUsageFetcher.swift +291 -0
  342. package/Sources/RunicCore/Providers/Copilot/CopilotVSCodeTokenReader.swift +386 -0
  343. package/Sources/RunicCore/Providers/Cursor/CursorProviderDescriptor.swift +65 -0
  344. package/Sources/RunicCore/Providers/Cursor/CursorStatusProbe.swift +594 -0
  345. package/Sources/RunicCore/Providers/DeepSeek/DeepSeekProviderDescriptor.swift +85 -0
  346. package/Sources/RunicCore/Providers/DeepSeek/DeepSeekUsageFetcher.swift +108 -0
  347. package/Sources/RunicCore/Providers/Factory/FactoryLocalStorageImporter.swift +398 -0
  348. package/Sources/RunicCore/Providers/Factory/FactoryProviderDescriptor.swift +65 -0
  349. package/Sources/RunicCore/Providers/Factory/FactoryStatusProbe.swift +1275 -0
  350. package/Sources/RunicCore/Providers/Fireworks/FireworksProviderDescriptor.swift +84 -0
  351. package/Sources/RunicCore/Providers/Fireworks/FireworksUsageFetcher.swift +75 -0
  352. package/Sources/RunicCore/Providers/Gemini/GeminiProviderDescriptor.swift +65 -0
  353. package/Sources/RunicCore/Providers/Gemini/GeminiStatusProbe.swift +835 -0
  354. package/Sources/RunicCore/Providers/Groq/GroqProviderDescriptor.swift +83 -0
  355. package/Sources/RunicCore/Providers/Groq/GroqUsageFetcher.swift +71 -0
  356. package/Sources/RunicCore/Providers/Kimi/KimiProviderDescriptor.swift +83 -0
  357. package/Sources/RunicCore/Providers/Kimi/KimiUsageFetcher.swift +77 -0
  358. package/Sources/RunicCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift +92 -0
  359. package/Sources/RunicCore/Providers/MiniMax/MiniMaxSettingsReader.swift +54 -0
  360. package/Sources/RunicCore/Providers/MiniMax/MiniMaxUsageFetcher.swift +171 -0
  361. package/Sources/RunicCore/Providers/MiniMax/MiniMaxWebUsageFetcher.swift +911 -0
  362. package/Sources/RunicCore/Providers/Mistral/MistralProviderDescriptor.swift +84 -0
  363. package/Sources/RunicCore/Providers/Mistral/MistralUsageFetcher.swift +75 -0
  364. package/Sources/RunicCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift +87 -0
  365. package/Sources/RunicCore/Providers/OpenRouter/OpenRouterUsageFetcher.swift +207 -0
  366. package/Sources/RunicCore/Providers/Perplexity/PerplexityProviderDescriptor.swift +84 -0
  367. package/Sources/RunicCore/Providers/Perplexity/PerplexityUsageFetcher.swift +75 -0
  368. package/Sources/RunicCore/Providers/ProviderBranding.swift +25 -0
  369. package/Sources/RunicCore/Providers/ProviderCLIConfig.swift +17 -0
  370. package/Sources/RunicCore/Providers/ProviderDescriptor.swift +129 -0
  371. package/Sources/RunicCore/Providers/ProviderFetchPlan.swift +263 -0
  372. package/Sources/RunicCore/Providers/ProviderSettingsSnapshot.swift +98 -0
  373. package/Sources/RunicCore/Providers/ProviderTokenResolver.swift +501 -0
  374. package/Sources/RunicCore/Providers/ProviderVersionDetector.swift +68 -0
  375. package/Sources/RunicCore/Providers/Providers.swift +184 -0
  376. package/Sources/RunicCore/Providers/Qwen/QwenProviderDescriptor.swift +84 -0
  377. package/Sources/RunicCore/Providers/Qwen/QwenSettingsReader.swift +30 -0
  378. package/Sources/RunicCore/Providers/Qwen/QwenUsageStats.swift +317 -0
  379. package/Sources/RunicCore/Providers/SambaNova/SambaNovaProviderDescriptor.swift +84 -0
  380. package/Sources/RunicCore/Providers/SambaNova/SambaNovaUsageFetcher.swift +75 -0
  381. package/Sources/RunicCore/Providers/Together/TogetherProviderDescriptor.swift +83 -0
  382. package/Sources/RunicCore/Providers/Together/TogetherUsageFetcher.swift +75 -0
  383. package/Sources/RunicCore/Providers/VertexAI/VertexAIProviderDescriptor.swift +123 -0
  384. package/Sources/RunicCore/Providers/VertexAI/VertexAISettingsReader.swift +48 -0
  385. package/Sources/RunicCore/Providers/VertexAI/VertexAIUsageFetcher.swift +144 -0
  386. package/Sources/RunicCore/Providers/XAI/XAIProviderDescriptor.swift +83 -0
  387. package/Sources/RunicCore/Providers/XAI/XAIUsageFetcher.swift +75 -0
  388. package/Sources/RunicCore/Providers/Zai/ZaiProviderDescriptor.swift +72 -0
  389. package/Sources/RunicCore/Providers/Zai/ZaiSettingsReader.swift +41 -0
  390. package/Sources/RunicCore/Providers/Zai/ZaiUsageStats.swift +724 -0
  391. package/Sources/RunicCore/Sync/BackgroundSyncManager.swift +385 -0
  392. package/Sources/RunicCore/Sync/SyncConflictResolver.swift +310 -0
  393. package/Sources/RunicCore/Sync/SyncProtocol.swift +288 -0
  394. package/Sources/RunicCore/Sync/SyncRecord.swift +405 -0
  395. package/Sources/RunicCore/Sync/SyncUsageExample.swift +294 -0
  396. package/Sources/RunicCore/Sync/TeamSyncRecords.swift +775 -0
  397. package/Sources/RunicCore/Sync/iCloudSyncEngine.swift +333 -0
  398. package/Sources/RunicCore/Team/TeamModels.swift +583 -0
  399. package/Sources/RunicCore/Team/TeamShowbackExporter.swift +495 -0
  400. package/Sources/RunicCore/Team/TeamStore.swift +792 -0
  401. package/Sources/RunicCore/TextParsing.swift +55 -0
  402. package/Sources/RunicCore/UsageFetcher.swift +694 -0
  403. package/Sources/RunicCore/UsageFormatter.swift +410 -0
  404. package/Sources/RunicCore/UsageLedger/ClaudeUsageLogSource.swift +329 -0
  405. package/Sources/RunicCore/UsageLedger/CodexUsageLogSource.swift +448 -0
  406. package/Sources/RunicCore/UsageLedger/OTelGenAILedgerAdapter.swift +550 -0
  407. package/Sources/RunicCore/UsageLedger/UsageLedger.swift +29 -0
  408. package/Sources/RunicCore/UsageLedger/UsageLedgerAggregator.swift +660 -0
  409. package/Sources/RunicCore/UsageLedger/UsageLedgerInsightsAdvisor.swift +209 -0
  410. package/Sources/RunicCore/UsageLedger/UsageLedgerModels.swift +417 -0
  411. package/Sources/RunicCore/UsageLedger/UsageLedgerProjectIdentity.swift +245 -0
  412. package/Sources/RunicCore/UsagePace.swift +81 -0
  413. package/Sources/RunicCore/Vendored/CostUsage/CostUsageCache.swift +73 -0
  414. package/Sources/RunicCore/Vendored/CostUsage/CostUsageJsonl.swift +74 -0
  415. package/Sources/RunicCore/Vendored/CostUsage/CostUsagePricing.swift +208 -0
  416. package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner+Claude.swift +455 -0
  417. package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner+Timestamp.swift +132 -0
  418. package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner.swift +589 -0
  419. package/Sources/RunicCore/WidgetSnapshot.swift +183 -0
  420. package/Sources/RunicMacroSupport/ProviderRegistrationMacros.swift +14 -0
  421. package/Sources/RunicMacros/ProviderRegistrationMacros.swift +216 -0
  422. package/Sources/RunicWidget/RunicWidgetBundle.swift +79 -0
  423. package/Sources/RunicWidget/RunicWidgetProvider.swift +283 -0
  424. package/Sources/RunicWidget/RunicWidgetViews.swift +680 -0
  425. package/Tests/RunicTests/AntigravityStatusProbeTests.swift +50 -0
  426. package/Tests/RunicTests/AppDelegateTests.swift +43 -0
  427. package/Tests/RunicTests/BrowserCookieOrderLabelTests.swift +22 -0
  428. package/Tests/RunicTests/CLIArgumentParsingTests.swift +70 -0
  429. package/Tests/RunicTests/CLICostTests.swift +65 -0
  430. package/Tests/RunicTests/CLIProviderSelectionTests.swift +22 -0
  431. package/Tests/RunicTests/CLISnapshotTests.swift +72 -0
  432. package/Tests/RunicTests/CLIWebFallbackTests.swift +58 -0
  433. package/Tests/RunicTests/ClaudeOAuthTests.swift +164 -0
  434. package/Tests/RunicTests/ClaudeResilienceTests.swift +30 -0
  435. package/Tests/RunicTests/ClaudeUsageTests.swift +370 -0
  436. package/Tests/RunicTests/CodexOAuthTests.swift +122 -0
  437. package/Tests/RunicTests/CodexUsageLogSourceTests.swift +82 -0
  438. package/Tests/RunicTests/CopilotTokenResolverTests.swift +75 -0
  439. package/Tests/RunicTests/CopilotVSCodeTokenReaderTests.swift +75 -0
  440. package/Tests/RunicTests/CostUsageDecodingTests.swift +394 -0
  441. package/Tests/RunicTests/CostUsagePricingTests.swift +50 -0
  442. package/Tests/RunicTests/CostUsageScannerTests.swift +700 -0
  443. package/Tests/RunicTests/CursorStatusProbeTests.swift +323 -0
  444. package/Tests/RunicTests/FactoryStatusProbeTests.swift +69 -0
  445. package/Tests/RunicTests/GeminiAPITestHelpers.swift +94 -0
  446. package/Tests/RunicTests/GeminiLoginAlertTests.swift +28 -0
  447. package/Tests/RunicTests/GeminiStatusProbeAPITests.swift +283 -0
  448. package/Tests/RunicTests/GeminiStatusProbePlanTests.swift +279 -0
  449. package/Tests/RunicTests/GeminiStatusProbeTests.swift +233 -0
  450. package/Tests/RunicTests/GeminiTestEnvironment.swift +87 -0
  451. package/Tests/RunicTests/GoogleWorkspaceStatusTests.swift +73 -0
  452. package/Tests/RunicTests/InstallOriginTests.swift +18 -0
  453. package/Tests/RunicTests/KeyboardShortcutsBundleTests.swift +9 -0
  454. package/Tests/RunicTests/LiveAccountTests.swift +24 -0
  455. package/Tests/RunicTests/LoadingPatternTests.swift +35 -0
  456. package/Tests/RunicTests/MenuCardModelTests.swift +545 -0
  457. package/Tests/RunicTests/MiniMaxCookieHeaderStoreTestSupport.swift +8 -0
  458. package/Tests/RunicTests/MiniMaxGroupIDStoreTestSupport.swift +8 -0
  459. package/Tests/RunicTests/MiniMaxParsingTests.swift +68 -0
  460. package/Tests/RunicTests/MiniMaxTokenStoreTestSupport.swift +8 -0
  461. package/Tests/RunicTests/OTelGenAILedgerAdapterTests.swift +112 -0
  462. package/Tests/RunicTests/OpenAIDashboardBrowserCookieImporterTests.swift +17 -0
  463. package/Tests/RunicTests/OpenAIDashboardFetcherCreditsWaitTests.swift +75 -0
  464. package/Tests/RunicTests/OpenAIDashboardOffscreenHostTests.swift +27 -0
  465. package/Tests/RunicTests/OpenAIDashboardParserTests.swift +134 -0
  466. package/Tests/RunicTests/OpenAIWebAccountSwitchTests.swift +53 -0
  467. package/Tests/RunicTests/OpenRouterTokenStoreTestSupport.swift +92 -0
  468. package/Tests/RunicTests/PathBuilderTests.swift +237 -0
  469. package/Tests/RunicTests/ProviderFetchPipelineTests.swift +160 -0
  470. package/Tests/RunicTests/ProviderIconResourcesTests.swift +38 -0
  471. package/Tests/RunicTests/ProviderMetadataStatusLinkTests.swift +16 -0
  472. package/Tests/RunicTests/ProviderRegistryTests.swift +29 -0
  473. package/Tests/RunicTests/ProviderSettingsDescriptorTests.swift +202 -0
  474. package/Tests/RunicTests/ProviderToggleStoreTests.swift +51 -0
  475. package/Tests/RunicTests/RunicTests.swift +120 -0
  476. package/Tests/RunicTests/SessionQuotaNotificationLogicTests.swift +36 -0
  477. package/Tests/RunicTests/SettingsStoreTests.swift +161 -0
  478. package/Tests/RunicTests/StatusItemAnimationTests.swift +49 -0
  479. package/Tests/RunicTests/StatusMenuTests.swift +434 -0
  480. package/Tests/RunicTests/StatusProbeTests.swift +377 -0
  481. package/Tests/RunicTests/SubprocessRunnerTests.swift +20 -0
  482. package/Tests/RunicTests/SubscriptionDetectionTests.swift +65 -0
  483. package/Tests/RunicTests/TTYCommandRunnerTests.swift +148 -0
  484. package/Tests/RunicTests/TTYIntegrationTests.swift +41 -0
  485. package/Tests/RunicTests/TeamShowbackExporterTests.swift +159 -0
  486. package/Tests/RunicTests/TextParsingTests.swift +12 -0
  487. package/Tests/RunicTests/UsageFormatterTests.swift +112 -0
  488. package/Tests/RunicTests/UsageLedgerAnomalyDetectionTests.swift +172 -0
  489. package/Tests/RunicTests/UsageLedgerForecastTests.swift +213 -0
  490. package/Tests/RunicTests/UsageLedgerInsightsAdvisorTests.swift +83 -0
  491. package/Tests/RunicTests/UsageLedgerProjectNameTests.swift +170 -0
  492. package/Tests/RunicTests/UsagePaceTests.swift +77 -0
  493. package/Tests/RunicTests/UsagePaceTextTests.swift +109 -0
  494. package/Tests/RunicTests/UsagePolicyEngineTests.swift +91 -0
  495. package/Tests/RunicTests/WidgetSnapshotTests.swift +45 -0
  496. package/Tests/RunicTests/ZaiProviderTests.swift +61 -0
  497. package/Tests/RunicTests/ZaiTokenStoreTestSupport.swift +6 -0
  498. package/TestsLinux/PlatformGatingTests.swift +46 -0
  499. package/appcast.xml +105 -0
  500. package/assets/logo-concept-stack.svg +24 -0
  501. package/assets/logo-minimal-bars.svg +15 -0
  502. package/assets/logo.svg +20 -0
  503. package/assets/logo2.svg +43 -0
  504. package/bin/docs-list +5 -0
  505. package/bin/install-runic-cli.sh +32 -0
  506. package/build-and-run.sh +369 -0
  507. package/docs/.nojekyll +1 -0
  508. package/docs/CNAME +1 -0
  509. package/docs/antigravity.md +75 -0
  510. package/docs/architecture.md +32 -0
  511. package/docs/claude.md +93 -0
  512. package/docs/cli-commands/integration_guide.md +371 -0
  513. package/docs/cli-commands/quick_reference.md +362 -0
  514. package/docs/cli-commands/readme.md +361 -0
  515. package/docs/cli.md +156 -0
  516. package/docs/cloudkit_sync_guide.md +495 -0
  517. package/docs/codebase_review.md +288 -0
  518. package/docs/codex-oauth.md +635 -0
  519. package/docs/codex.md +104 -0
  520. package/docs/codexbar.png +0 -0
  521. package/docs/copilot.md +44 -0
  522. package/docs/cursor.md +46 -0
  523. package/docs/design/designcomparisonchart.md +510 -0
  524. package/docs/design/implementationguide.md +792 -0
  525. package/docs/design/menubaricondesignconcepts.md +796 -0
  526. package/docs/design/mockups/hybridiconconcept.svg +62 -0
  527. package/docs/design/mockups/infinityiconconcept.svg +41 -0
  528. package/docs/design/mockups/infinityiconstates.svg +129 -0
  529. package/docs/design/mockups/wavelogorefined.svg +48 -0
  530. package/docs/design/quick_summary.md +426 -0
  531. package/docs/design/readme.md +478 -0
  532. package/docs/design/sizespecifications.md +575 -0
  533. package/docs/design-system.md +836 -0
  534. package/docs/factory.md +103 -0
  535. package/docs/gemini.md +73 -0
  536. package/docs/icon.md +37 -0
  537. package/docs/icon.png +0 -0
  538. package/docs/index.html +175 -0
  539. package/docs/keychain_fix.md +274 -0
  540. package/docs/minimax.md +71 -0
  541. package/docs/packaging.md +25 -0
  542. package/docs/progress-bar-improvements.md +240 -0
  543. package/docs/progress-bar-quick-reference.md +283 -0
  544. package/docs/progress-bar-technical-details.md +432 -0
  545. package/docs/progress-bar-visual-test.swift +375 -0
  546. package/docs/provider.md +202 -0
  547. package/docs/providers.md +86 -0
  548. package/docs/refactor/macros.md +35 -0
  549. package/docs/refresh-loop.md +22 -0
  550. package/docs/releasing-homebrew.md +35 -0
  551. package/docs/releasing.md +121 -0
  552. package/docs/research/concrete_todo_2026-02-22.md +47 -0
  553. package/docs/research/core-feature-research.md +248 -0
  554. package/docs/research/feature_uiux_roadmap_2026-02-22.md +202 -0
  555. package/docs/research/parity_matrix.md +49 -0
  556. package/docs/research/provider_expansion_candidates_2026-02-22.md +80 -0
  557. package/docs/research/provider_model_token_project_matrix_2026-02-23.md +46 -0
  558. package/docs/research/release_gate_checklist.md +57 -0
  559. package/docs/site.css +505 -0
  560. package/docs/sparkle.md +28 -0
  561. package/docs/status.md +28 -0
  562. package/docs/sync/implementation.md +393 -0
  563. package/docs/sync/readme.md +317 -0
  564. package/docs/sync_files_summary.md +391 -0
  565. package/docs/sync_quick_reference.md +225 -0
  566. package/docs/todo.md +11 -0
  567. package/docs/ui.md +29 -0
  568. package/docs/web-integration.md +9 -0
  569. package/docs/widgets.md +85 -0
  570. package/docs/workflows/error-handling-improvements.md +434 -0
  571. package/docs/workflows/folder_structure.md +299 -0
  572. package/docs/workflows/loading_states.md +446 -0
  573. package/docs/workflows/loading_states_examples.md +565 -0
  574. package/docs/workflows/loading_states_flow.md +430 -0
  575. package/docs/workflows/performance_optimizations.md +457 -0
  576. package/docs/workflows/phase1-cli-testing-report.md +313 -0
  577. package/docs/workflows/quick_reference.md +228 -0
  578. package/docs/workflows/readme.md +59 -0
  579. package/docs/workflows/recent_improvements.md +275 -0
  580. package/docs/workflows/sync_enablement_summary.md +400 -0
  581. package/docs/zai.md +43 -0
  582. package/package.json +36 -0
  583. package/runic.png +0 -0
  584. package/version.env +2 -0
@@ -0,0 +1,810 @@
1
+ import AppKit
2
+ import RunicCore
3
+ import SwiftUI
4
+
5
+ // MARK: - Chart submenu builders
6
+
7
+ extension StatusItemController {
8
+ @discardableResult
9
+ func addCreditsHistorySubmenu(to menu: NSMenu) -> Bool {
10
+ guard let submenu = self.makeCreditsHistorySubmenu() else { return false }
11
+ let item = NSMenuItem(title: "Credits history", action: nil, keyEquivalent: "")
12
+ item.isEnabled = true
13
+ item.submenu = submenu
14
+ menu.addItem(item)
15
+ return true
16
+ }
17
+
18
+ @discardableResult
19
+ func addUsageBreakdownSubmenu(to menu: NSMenu) -> Bool {
20
+ guard let submenu = self.makeUsageBreakdownSubmenu() else { return false }
21
+ let item = NSMenuItem(title: "Usage breakdown", action: nil, keyEquivalent: "")
22
+ item.isEnabled = true
23
+ item.submenu = submenu
24
+ menu.addItem(item)
25
+ return true
26
+ }
27
+
28
+ @discardableResult
29
+ func addCostHistorySubmenu(to menu: NSMenu, provider: UsageProvider) -> Bool {
30
+ guard let submenu = self.makeCostHistorySubmenu(provider: provider) else { return false }
31
+ let item = NSMenuItem(title: "Usage history (30 days)", action: nil, keyEquivalent: "")
32
+ item.isEnabled = true
33
+ item.submenu = submenu
34
+ menu.addItem(item)
35
+ return true
36
+ }
37
+
38
+ func makeUsageSubmenu(
39
+ provider: UsageProvider,
40
+ snapshot: UsageSnapshot?,
41
+ webItems: OpenAIWebMenuItems) -> NSMenu?
42
+ {
43
+ if provider == .codex, webItems.hasUsageBreakdown {
44
+ return self.makeUsageBreakdownSubmenu()
45
+ }
46
+ if provider == .zai {
47
+ return self.makeZaiUsageDetailsSubmenu(snapshot: snapshot)
48
+ }
49
+ return nil
50
+ }
51
+
52
+ func makeInsightsSubmenu(provider: UsageProvider) -> NSMenu? {
53
+ let daily = self.store.ledgerDailySummary(for: provider)
54
+ let activeBlock = self.store.ledgerActiveBlock(for: provider)
55
+ let modelBreakdown = self.store.ledgerModelBreakdown(for: provider)
56
+ .filter { $0.provider == provider }
57
+ let projectBreakdown = self.store.ledgerProjectBreakdown(for: provider)
58
+ .filter { $0.provider == provider }
59
+ let reliability = self.store.ledgerReliabilityScore(for: provider)
60
+ let routing = self.store.ledgerRoutingRecommendation(for: provider)
61
+ let hasActiveBlock = activeBlock?.isActive == true
62
+ guard daily != nil || hasActiveBlock || !modelBreakdown.isEmpty || !projectBreakdown.isEmpty || reliability != nil || routing != nil else {
63
+ return nil
64
+ }
65
+
66
+ let submenu = NSMenu()
67
+ submenu.autoenablesItems = false
68
+ let limit = max(1, self.settings.insightsMenuMaxItems)
69
+ let reportDays = max(1, self.settings.insightsReportDays)
70
+ let hasOverflow = modelBreakdown.count > limit || projectBreakdown.count > limit
71
+
72
+ let titleItem = NSMenuItem(title: "Local insights (today)", action: nil, keyEquivalent: "")
73
+ titleItem.isEnabled = false
74
+ submenu.addItem(titleItem)
75
+
76
+ if let daily {
77
+ let totalTokens = UsageFormatter.tokenCountString(daily.totals.totalTokens)
78
+ let inputTokens = UsageFormatter.tokenCountString(daily.totals.inputTokens)
79
+ let outputTokens = UsageFormatter.tokenCountString(daily.totals.outputTokens)
80
+ var line = "Today: \(totalTokens) tokens · in \(inputTokens) · out \(outputTokens)"
81
+ if let cost = daily.totals.costUSD {
82
+ line += " · \(UsageFormatter.usdString(cost))"
83
+ if let per1K = UsageFormatter.usdPer1KTokensString(
84
+ costUSD: cost,
85
+ tokenCount: daily.totals.totalTokens)
86
+ {
87
+ line += " · \(per1K)"
88
+ }
89
+ }
90
+ let item = NSMenuItem(title: line, action: nil, keyEquivalent: "")
91
+ item.isEnabled = false
92
+ submenu.addItem(item)
93
+ }
94
+
95
+ if let activeBlock, activeBlock.isActive {
96
+ let tokens = UsageFormatter.tokenCountString(activeBlock.totals.totalTokens)
97
+ var line = "Block: \(tokens) tokens · \(activeBlock.entryCount) req"
98
+ if let cost = activeBlock.totals.costUSD {
99
+ line += " · \(UsageFormatter.usdString(cost))"
100
+ if let perRequest = UsageFormatter.usdPerRequestString(
101
+ costUSD: cost,
102
+ requestCount: activeBlock.entryCount)
103
+ {
104
+ line += " · \(perRequest)"
105
+ }
106
+ }
107
+ if let burn = UsageFormatter.usdPerHourFromTokensString(
108
+ costUSD: activeBlock.totals.costUSD,
109
+ tokenCount: activeBlock.totals.totalTokens,
110
+ tokensPerMinute: activeBlock.tokensPerMinute)
111
+ {
112
+ line += " · burn \(burn)"
113
+ }
114
+ let item = NSMenuItem(title: line, action: nil, keyEquivalent: "")
115
+ item.isEnabled = false
116
+ submenu.addItem(item)
117
+ }
118
+
119
+ if let reliability {
120
+ let item = NSMenuItem(
121
+ title: "Reliability: \(reliability.score)/100 · \(reliability.grade)",
122
+ action: nil,
123
+ keyEquivalent: "")
124
+ item.isEnabled = false
125
+ submenu.addItem(item)
126
+ }
127
+ if let routing {
128
+ let from = UsageFormatter.modelDisplayName(routing.fromModel)
129
+ let to = UsageFormatter.modelDisplayName(routing.toModel)
130
+ let savings = UsageFormatter.usdString(routing.estimatedSavingsUSD)
131
+ let item = NSMenuItem(
132
+ title: "Routing: shift \(routing.shiftPercent)% \(from) -> \(to) · save \(savings)",
133
+ action: nil,
134
+ keyEquivalent: "")
135
+ item.isEnabled = false
136
+ submenu.addItem(item)
137
+ }
138
+
139
+ if !modelBreakdown.isEmpty {
140
+ submenu.addItem(NSMenuItem.separator())
141
+ let header = NSMenuItem(title: "Models by project", action: nil, keyEquivalent: "")
142
+ header.isEnabled = false
143
+ submenu.addItem(header)
144
+
145
+ let limited = modelBreakdown.prefix(limit)
146
+ for summary in limited {
147
+ let project = self.displayProjectName(
148
+ projectID: summary.projectID,
149
+ projectName: summary.projectName,
150
+ confidence: summary.projectNameConfidence,
151
+ source: summary.projectNameSource,
152
+ provenance: summary.projectNameProvenance,
153
+ includeAttribution: true)
154
+ let tokens = UsageFormatter.tokenCountString(summary.totals.totalTokens)
155
+ let costText = summary.totals.costUSD.map { UsageFormatter.usdString($0) }
156
+ let modelName = UsageFormatter.modelDisplayName(summary.model)
157
+ var title = "\(project) · \(modelName): \(tokens) tokens · \(summary.entryCount) req"
158
+ if let context = UsageFormatter.modelContextLabel(for: summary.model) {
159
+ title += " · \(context)"
160
+ }
161
+ if let costText { title += " · \(costText)" }
162
+ if let per1K = UsageFormatter.usdPer1KTokensString(
163
+ costUSD: summary.totals.costUSD,
164
+ tokenCount: summary.totals.totalTokens)
165
+ {
166
+ title += " · \(per1K)"
167
+ }
168
+ let item = NSMenuItem(title: title, action: nil, keyEquivalent: "")
169
+ item.isEnabled = false
170
+ submenu.addItem(item)
171
+ }
172
+ }
173
+
174
+ if !projectBreakdown.isEmpty {
175
+ submenu.addItem(NSMenuItem.separator())
176
+ let header = NSMenuItem(title: "Projects", action: nil, keyEquivalent: "")
177
+ header.isEnabled = false
178
+ submenu.addItem(header)
179
+
180
+ let limited = projectBreakdown.prefix(limit)
181
+ for summary in limited {
182
+ let project = self.displayProjectName(
183
+ projectID: summary.projectID,
184
+ projectName: summary.projectName,
185
+ confidence: summary.projectNameConfidence,
186
+ source: summary.projectNameSource,
187
+ provenance: summary.projectNameProvenance,
188
+ includeAttribution: true)
189
+ let tokens = UsageFormatter.tokenCountString(summary.totals.totalTokens)
190
+ let costText = summary.totals.costUSD.map { UsageFormatter.usdString($0) }
191
+ let modelsText = summary.modelsUsed.isEmpty
192
+ ? nil
193
+ : Self.renderedModelsLine(for: summary.modelsUsed)
194
+ var title = "\(project): \(tokens) tokens · \(summary.entryCount) req"
195
+ if let costText { title += " · \(costText)" }
196
+ if let per1K = UsageFormatter.usdPer1KTokensString(
197
+ costUSD: summary.totals.costUSD,
198
+ tokenCount: summary.totals.totalTokens)
199
+ {
200
+ title += " · \(per1K)"
201
+ }
202
+ if let modelsText { title += " · \(modelsText)" }
203
+ let item = NSMenuItem(title: title, action: nil, keyEquivalent: "")
204
+ item.isEnabled = false
205
+ submenu.addItem(item)
206
+ }
207
+ }
208
+
209
+ submenu.addItem(NSMenuItem.separator())
210
+ let reportTitle = hasOverflow
211
+ ? "More… (last \(reportDays) days)"
212
+ : "Open insights report (last \(reportDays) days)…"
213
+ let openItem = NSMenuItem(
214
+ title: reportTitle,
215
+ action: #selector(self.openInsightsReport(_:)),
216
+ keyEquivalent: "")
217
+ openItem.target = self
218
+ openItem.representedObject = provider.rawValue
219
+ submenu.addItem(openItem)
220
+
221
+ return submenu
222
+ }
223
+
224
+ func displayProjectName(
225
+ projectID: String?,
226
+ projectName: String?,
227
+ confidence: UsageLedgerProjectNameConfidence? = nil,
228
+ source: UsageLedgerProjectNameSource? = nil,
229
+ provenance: String? = nil,
230
+ includeAttribution: Bool = false) -> String
231
+ {
232
+ let trimmedProjectName = projectName?.trimmingCharacters(in: .whitespacesAndNewlines)
233
+ let displayName: String
234
+ if let trimmedProjectName, !trimmedProjectName.isEmpty {
235
+ displayName = trimmedProjectName
236
+ } else if let projectID, !projectID.isEmpty {
237
+ if let budgetName = ProjectBudgetStore.getBudget(projectID: projectID)?.projectName?
238
+ .trimmingCharacters(in: .whitespacesAndNewlines),
239
+ !budgetName.isEmpty
240
+ {
241
+ displayName = budgetName
242
+ } else if let fallback = UsageLedgerProjectIdentityResolver.fallbackDisplayName(projectID: projectID) {
243
+ displayName = fallback
244
+ } else {
245
+ displayName = "Unknown project"
246
+ }
247
+ } else {
248
+ displayName = "Unknown project"
249
+ }
250
+ guard includeAttribution else { return displayName }
251
+ guard let annotation = self.projectNameAnnotation(
252
+ displayName: displayName,
253
+ projectID: projectID,
254
+ confidence: confidence,
255
+ source: source,
256
+ provenance: provenance)
257
+ else {
258
+ return displayName
259
+ }
260
+ return "\(displayName) [\(annotation)]"
261
+ }
262
+
263
+ private func projectNameAnnotation(
264
+ displayName: String,
265
+ projectID: String?,
266
+ confidence: UsageLedgerProjectNameConfidence?,
267
+ source: UsageLedgerProjectNameSource?,
268
+ provenance: String?) -> String?
269
+ {
270
+ let normalizedSource = source ?? .unknown
271
+ let normalizedConfidence = confidence ?? .none
272
+
273
+ let shouldAnnotateSource = normalizedSource != .projectName && normalizedSource != .budgetOverride
274
+ let shouldAnnotateConfidence = normalizedConfidence != .high
275
+ let isUnknown = displayName == "Unknown project"
276
+ guard shouldAnnotateSource || shouldAnnotateConfidence || isUnknown else { return nil }
277
+
278
+ var parts: [String] = []
279
+ if shouldAnnotateSource {
280
+ parts.append("source \(self.projectSourceLabel(normalizedSource))")
281
+ }
282
+ if shouldAnnotateConfidence {
283
+ parts.append("confidence \(self.projectConfidenceLabel(normalizedConfidence))")
284
+ }
285
+ if isUnknown, let fingerprint = self.projectIDFingerprint(projectID) {
286
+ parts.append("id \(fingerprint)")
287
+ }
288
+ if let provenance = provenance?.trimmingCharacters(in: .whitespacesAndNewlines),
289
+ !provenance.isEmpty
290
+ {
291
+ parts.append("via \(provenance)")
292
+ }
293
+ return parts.isEmpty ? nil : parts.joined(separator: " · ")
294
+ }
295
+
296
+ private func projectSourceLabel(_ source: UsageLedgerProjectNameSource) -> String {
297
+ switch source {
298
+ case .projectName: "project name"
299
+ case .projectID: "project id"
300
+ case .inferredFromPath: "path-derived"
301
+ case .inferredFromName: "name-derived"
302
+ case .budgetOverride: "budget override"
303
+ case .unknown: "unknown"
304
+ }
305
+ }
306
+
307
+ private func projectConfidenceLabel(_ confidence: UsageLedgerProjectNameConfidence) -> String {
308
+ switch confidence {
309
+ case .high: "high"
310
+ case .medium: "medium"
311
+ case .low: "low"
312
+ case .none: "none"
313
+ }
314
+ }
315
+
316
+ private func projectIDFingerprint(_ projectID: String?) -> String? {
317
+ guard let projectID = projectID?.trimmingCharacters(in: .whitespacesAndNewlines), !projectID.isEmpty else {
318
+ return nil
319
+ }
320
+
321
+ var hash: UInt64 = 0xcbf29ce484222325
322
+ for byte in projectID.lowercased().utf8 {
323
+ hash ^= UInt64(byte)
324
+ hash = hash &* 0x100000001b3
325
+ }
326
+ return String(format: "%08llx", hash)
327
+ }
328
+
329
+ // MARK: - Private chart builders
330
+
331
+ func makeUsageBreakdownSubmenu() -> NSMenu? {
332
+ let breakdown = self.store.openAIDashboard?.usageBreakdown ?? []
333
+ let width = Self.menuCardBaseWidth
334
+ guard !breakdown.isEmpty else { return nil }
335
+
336
+ let submenu = NSMenu()
337
+ submenu.delegate = self
338
+ let chartView = UsageBreakdownChartMenuView(breakdown: breakdown, width: width)
339
+ let hosting = MenuHostingView(rootView: chartView)
340
+ let controller = NSHostingController(rootView: chartView)
341
+ let size = controller.sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude))
342
+ hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: size.height))
343
+
344
+ let chartItem = NSMenuItem()
345
+ chartItem.view = hosting
346
+ chartItem.isEnabled = false
347
+ chartItem.representedObject = "usageBreakdownChart"
348
+ submenu.addItem(chartItem)
349
+ return submenu
350
+ }
351
+
352
+ func makeCreditsHistorySubmenu() -> NSMenu? {
353
+ let breakdown = self.store.openAIDashboard?.dailyBreakdown ?? []
354
+ let width = Self.menuCardBaseWidth
355
+ guard !breakdown.isEmpty else { return nil }
356
+
357
+ let submenu = NSMenu()
358
+ submenu.delegate = self
359
+ let chartView = CreditsHistoryChartMenuView(breakdown: breakdown, width: width)
360
+ let hosting = MenuHostingView(rootView: chartView)
361
+ let controller = NSHostingController(rootView: chartView)
362
+ let size = controller.sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude))
363
+ hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: size.height))
364
+
365
+ let chartItem = NSMenuItem()
366
+ chartItem.view = hosting
367
+ chartItem.isEnabled = false
368
+ chartItem.representedObject = "creditsHistoryChart"
369
+ submenu.addItem(chartItem)
370
+ return submenu
371
+ }
372
+
373
+ func makeCostHistorySubmenu(provider: UsageProvider) -> NSMenu? {
374
+ guard provider == .codex || provider == .claude else { return nil }
375
+ let width = Self.menuCardBaseWidth
376
+ guard let tokenSnapshot = self.store.tokenSnapshot(for: provider) else { return nil }
377
+ guard !tokenSnapshot.daily.isEmpty else { return nil }
378
+
379
+ let submenu = NSMenu()
380
+ submenu.delegate = self
381
+ let chartView = CostHistoryChartMenuView(
382
+ provider: provider,
383
+ daily: tokenSnapshot.daily,
384
+ totalCostUSD: tokenSnapshot.last30DaysCostUSD,
385
+ width: width)
386
+ let hosting = MenuHostingView(rootView: chartView)
387
+ let controller = NSHostingController(rootView: chartView)
388
+ let size = controller.sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude))
389
+ hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: size.height))
390
+
391
+ let chartItem = NSMenuItem()
392
+ chartItem.view = hosting
393
+ chartItem.isEnabled = false
394
+ chartItem.representedObject = "costHistoryChart"
395
+ submenu.addItem(chartItem)
396
+ return submenu
397
+ }
398
+
399
+ // MARK: - Timeline & Activity submenus
400
+
401
+ @discardableResult
402
+ func addUsageTimelineSubmenu(to menu: NSMenu, provider: UsageProvider) -> Bool {
403
+ let dailySummaries = self.store.ledgerAllDailySummary(for: provider)
404
+ let hourlySummaries = self.store.ledgerHourlySummary(for: provider)
405
+ guard !dailySummaries.isEmpty || !hourlySummaries.isEmpty else { return false }
406
+
407
+ let width = Self.menuCardBaseWidth
408
+ let submenu = NSMenu()
409
+ submenu.delegate = self
410
+ let chartView = UsageTimelineChartMenuView(
411
+ dailySummaries: dailySummaries,
412
+ hourlySummaries: hourlySummaries,
413
+ width: width)
414
+ let hosting = MenuHostingView(rootView: chartView)
415
+ let controller = NSHostingController(rootView: chartView)
416
+ let size = controller.sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude))
417
+ hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: size.height))
418
+
419
+ let chartItem = NSMenuItem()
420
+ chartItem.view = hosting
421
+ chartItem.isEnabled = false
422
+ chartItem.representedObject = "usageTimelineChart"
423
+ submenu.addItem(chartItem)
424
+
425
+ let item = NSMenuItem(title: "Usage timeline", action: nil, keyEquivalent: "")
426
+ item.isEnabled = true
427
+ item.submenu = submenu
428
+ menu.addItem(item)
429
+ return true
430
+ }
431
+
432
+ @discardableResult
433
+ func addHourlyActivitySubmenu(to menu: NSMenu, provider: UsageProvider) -> Bool {
434
+ let hourlySummaries = self.store.ledgerHourlySummary(for: provider)
435
+ guard !hourlySummaries.isEmpty else { return false }
436
+
437
+ let width = Self.menuCardBaseWidth
438
+ let submenu = NSMenu()
439
+ submenu.delegate = self
440
+ let chartView = HourlyActivityChartMenuView(hourlySummaries: hourlySummaries, width: width)
441
+ let hosting = MenuHostingView(rootView: chartView)
442
+ let controller = NSHostingController(rootView: chartView)
443
+ let size = controller.sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude))
444
+ hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: size.height))
445
+
446
+ let chartItem = NSMenuItem()
447
+ chartItem.view = hosting
448
+ chartItem.isEnabled = false
449
+ chartItem.representedObject = "hourlyActivityChart"
450
+ submenu.addItem(chartItem)
451
+
452
+ let item = NSMenuItem(title: "Today by hour", action: nil, keyEquivalent: "")
453
+ item.isEnabled = true
454
+ item.submenu = submenu
455
+ menu.addItem(item)
456
+ return true
457
+ }
458
+
459
+ @discardableResult
460
+ func addWeeklyActivitySubmenu(to menu: NSMenu, provider: UsageProvider) -> Bool {
461
+ let dailySummaries = self.store.ledgerAllDailySummary(for: provider)
462
+ guard !dailySummaries.isEmpty else { return false }
463
+
464
+ let width = Self.menuCardBaseWidth
465
+ let submenu = NSMenu()
466
+ submenu.delegate = self
467
+ let chartView = WeeklyActivityChartMenuView(dailySummaries: dailySummaries, width: width)
468
+ let hosting = MenuHostingView(rootView: chartView)
469
+ let controller = NSHostingController(rootView: chartView)
470
+ let size = controller.sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude))
471
+ hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: size.height))
472
+
473
+ let chartItem = NSMenuItem()
474
+ chartItem.view = hosting
475
+ chartItem.isEnabled = false
476
+ chartItem.representedObject = "weeklyActivityChart"
477
+ submenu.addItem(chartItem)
478
+
479
+ let item = NSMenuItem(title: "Last 7 days", action: nil, keyEquivalent: "")
480
+ item.isEnabled = true
481
+ item.submenu = submenu
482
+ menu.addItem(item)
483
+ return true
484
+ }
485
+
486
+ // MARK: - Subscription Utilization & Window Comparison
487
+
488
+ @discardableResult
489
+ func addSubscriptionUtilizationSubmenu(to menu: NSMenu, provider: UsageProvider) -> Bool {
490
+ let dailySummaries = self.store.ledgerAllDailySummary(for: provider)
491
+ guard !dailySummaries.isEmpty else { return false }
492
+ let snapshot = self.store.snapshot(for: provider)
493
+ let currentUsedPercent = snapshot?.primary.usedPercent ?? 0
494
+ let todayTokens = self.store.ledgerDailySummary(for: provider)?.totals.totalTokens ?? 0
495
+ guard todayTokens > 0 else { return false }
496
+
497
+ let width = Self.menuCardBaseWidth
498
+ let submenu = NSMenu()
499
+ submenu.delegate = self
500
+ let chartView = SubscriptionUtilizationChartMenuView(
501
+ dailySummaries: dailySummaries,
502
+ currentUsedPercent: currentUsedPercent,
503
+ todayTokens: todayTokens,
504
+ width: width)
505
+ let hosting = MenuHostingView(rootView: chartView)
506
+ let controller = NSHostingController(rootView: chartView)
507
+ let size = controller.sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude))
508
+ hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: size.height))
509
+
510
+ let chartItem = NSMenuItem()
511
+ chartItem.view = hosting
512
+ chartItem.isEnabled = false
513
+ chartItem.representedObject = "subscriptionUtilizationChart"
514
+ submenu.addItem(chartItem)
515
+
516
+ let item = NSMenuItem(title: "Subscription utilization", action: nil, keyEquivalent: "")
517
+ item.isEnabled = true
518
+ item.submenu = submenu
519
+ menu.addItem(item)
520
+ return true
521
+ }
522
+
523
+ @discardableResult
524
+ func addUsageWindowComparisonSubmenu(to menu: NSMenu, provider: UsageProvider) -> Bool {
525
+ let dailySummaries = self.store.ledgerAllDailySummary(for: provider)
526
+ let snapshot = self.store.snapshot(for: provider)
527
+ guard !dailySummaries.isEmpty, let snapshot else { return false }
528
+ let todayTokens = self.store.ledgerDailySummary(for: provider)?.totals.totalTokens ?? 0
529
+ guard todayTokens > 0 else { return false }
530
+
531
+ let primaryLabel = snapshot.primary.label ?? snapshot.primary.resetDescription ?? "Session"
532
+ let secondaryLabel = snapshot.secondary?.label ?? snapshot.secondary?.resetDescription
533
+ let primaryPercent = snapshot.primary.usedPercent
534
+ let secondaryPercent = snapshot.secondary?.usedPercent
535
+ guard primaryPercent > 0 || (secondaryPercent ?? 0) > 0 else { return false }
536
+
537
+ let width = Self.menuCardBaseWidth
538
+ let submenu = NSMenu()
539
+ submenu.delegate = self
540
+ let chartView = UsageWindowComparisonChartMenuView(
541
+ dailySummaries: dailySummaries,
542
+ primaryLabel: primaryLabel,
543
+ secondaryLabel: secondaryLabel,
544
+ primaryPercent: primaryPercent,
545
+ secondaryPercent: secondaryPercent,
546
+ width: width)
547
+ let hosting = MenuHostingView(rootView: chartView)
548
+ let controller = NSHostingController(rootView: chartView)
549
+ let size = controller.sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude))
550
+ hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: size.height))
551
+
552
+ let chartItem = NSMenuItem()
553
+ chartItem.view = hosting
554
+ chartItem.isEnabled = false
555
+ chartItem.representedObject = "usageWindowComparisonChart"
556
+ submenu.addItem(chartItem)
557
+
558
+ let item = NSMenuItem(title: "Usage windows", action: nil, keyEquivalent: "")
559
+ item.isEnabled = true
560
+ item.submenu = submenu
561
+ menu.addItem(item)
562
+ return true
563
+ }
564
+
565
+ // MARK: - Project & Model submenus (Phase 4)
566
+
567
+ func addProjectBreakdownSubmenu(to menu: NSMenu, provider: UsageProvider) -> Bool {
568
+ let breakdown = self.store.ledgerProjectBreakdown(for: provider)
569
+ guard !breakdown.isEmpty else { return false }
570
+
571
+ let width = Self.menuCardBaseWidth
572
+ let submenu = NSMenu()
573
+ submenu.delegate = self
574
+ let chartView = ProjectBreakdownMenuView(breakdown: breakdown, width: width)
575
+ let hosting = MenuHostingView(rootView: chartView)
576
+ let controller = NSHostingController(rootView: chartView)
577
+ let size = controller.sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude))
578
+ hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: size.height))
579
+
580
+ let chartItem = NSMenuItem()
581
+ chartItem.view = hosting
582
+ chartItem.isEnabled = false
583
+ chartItem.representedObject = "projectBreakdownChart"
584
+ submenu.addItem(chartItem)
585
+
586
+ let item = NSMenuItem(title: "Projects", action: nil, keyEquivalent: "")
587
+ item.isEnabled = true
588
+ item.submenu = submenu
589
+ menu.addItem(item)
590
+ return true
591
+ }
592
+
593
+ func addModelBreakdownSubmenu(to menu: NSMenu, provider: UsageProvider) -> Bool {
594
+ let breakdown = self.store.ledgerModelBreakdown(for: provider)
595
+ let submenu = NSMenu()
596
+ submenu.delegate = self
597
+
598
+ if !breakdown.isEmpty {
599
+ let width = Self.menuCardBaseWidth
600
+ let chartView = ModelBreakdownMenuView(breakdown: breakdown, width: width)
601
+ let hosting = MenuHostingView(rootView: chartView)
602
+ let controller = NSHostingController(rootView: chartView)
603
+ let size = controller.sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude))
604
+ hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: size.height))
605
+
606
+ let chartItem = NSMenuItem()
607
+ chartItem.view = hosting
608
+ chartItem.isEnabled = false
609
+ chartItem.representedObject = "modelBreakdownChart"
610
+ submenu.addItem(chartItem)
611
+ } else {
612
+ let quotaWindows = self.modelQuotaWindows(for: provider)
613
+ guard !quotaWindows.isEmpty else { return false }
614
+
615
+ let titleItem = NSMenuItem(title: "Quota windows", action: nil, keyEquivalent: "")
616
+ titleItem.isEnabled = false
617
+ submenu.addItem(titleItem)
618
+
619
+ for window in quotaWindows {
620
+ let rawLabel = window.label?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Model"
621
+ let label = UsageFormatter.modelDisplayName(rawLabel)
622
+ let used = Int(window.usedPercent.rounded())
623
+ let remaining = Int(window.remainingPercent.rounded())
624
+ var line = "\(label): \(used)% used · \(remaining)% left"
625
+ if let resetsAt = window.resetsAt {
626
+ line += " · reset \(UsageFormatter.resetCountdownDescription(from: resetsAt))"
627
+ } else if let resetDescription = window.resetDescription?
628
+ .trimmingCharacters(in: .whitespacesAndNewlines),
629
+ !resetDescription.isEmpty
630
+ {
631
+ line += " · \(resetDescription)"
632
+ }
633
+ let detailItem = NSMenuItem(title: line, action: nil, keyEquivalent: "")
634
+ detailItem.isEnabled = false
635
+ submenu.addItem(detailItem)
636
+ }
637
+ }
638
+
639
+ let item = NSMenuItem(title: "Models", action: nil, keyEquivalent: "")
640
+ item.isEnabled = true
641
+ item.submenu = submenu
642
+ menu.addItem(item)
643
+ return true
644
+ }
645
+
646
+ // MARK: - Private helpers
647
+
648
+ private static func renderedModelsLine(for modelsUsed: [String]) -> String {
649
+ var seen: Set<String> = []
650
+ var rendered: [String] = []
651
+ for model in modelsUsed {
652
+ let trimmed = model.trimmingCharacters(in: .whitespacesAndNewlines)
653
+ guard !trimmed.isEmpty, seen.insert(trimmed).inserted else { continue }
654
+ var text = UsageFormatter.modelDisplayName(trimmed)
655
+ if let context = UsageFormatter.modelContextLabel(for: trimmed) {
656
+ text += " \(context)"
657
+ }
658
+ rendered.append(text)
659
+ if rendered.count >= 3 { break }
660
+ }
661
+ return rendered.joined(separator: ", ")
662
+ }
663
+
664
+ private func modelQuotaWindows(for provider: UsageProvider) -> [RateWindow] {
665
+ guard let snapshot = self.store.snapshot(for: provider) else { return [] }
666
+ let windows = [snapshot.primary, snapshot.secondary, snapshot.tertiary].compactMap { $0 }
667
+ var seen: Set<String> = []
668
+ var result: [RateWindow] = []
669
+
670
+ for window in windows {
671
+ guard let label = window.label?.trimmingCharacters(in: .whitespacesAndNewlines),
672
+ !label.isEmpty
673
+ else {
674
+ continue
675
+ }
676
+ let normalized = label.lowercased()
677
+ guard seen.insert(normalized).inserted else { continue }
678
+ result.append(window)
679
+ }
680
+ return result
681
+ }
682
+
683
+ private func makeZaiUsageDetailsSubmenu(snapshot: UsageSnapshot?) -> NSMenu? {
684
+ guard let zai = snapshot?.zaiUsage else { return nil }
685
+ let hasMCPDetails = zai.timeLimit.map { !$0.usageDetails.isEmpty } ?? false
686
+ let hasModelUsage = zai.modelUsage.map { !$0.entries.isEmpty } ?? false
687
+ let hasToolUsage = zai.toolUsage.map { !$0.entries.isEmpty } ?? false
688
+ guard hasMCPDetails || hasModelUsage || hasToolUsage else { return nil }
689
+
690
+ let submenu = NSMenu()
691
+ submenu.delegate = self
692
+
693
+ // MARK: Model usage (24h) — tokens, prompts, estimated cost per model
694
+ if let modelUsage = zai.modelUsage, !modelUsage.entries.isEmpty {
695
+ let headerFont = NSFont.systemFont(ofSize: NSFont.systemFontSize, weight: .semibold)
696
+ let header = NSMenuItem(title: "Models (24h)", action: nil, keyEquivalent: "")
697
+ header.attributedTitle = NSAttributedString(string: "Models (24h)", attributes: [.font: headerFont])
698
+ header.isEnabled = false
699
+ submenu.addItem(header)
700
+
701
+ let totalTokensStr = UsageFormatter.tokenCountString(modelUsage.totalTokens)
702
+ let totalPromptsStr = "\(modelUsage.totalPrompts) prompts"
703
+ let totalCostStr = modelUsage.totalEstimatedCostUSD > 0
704
+ ? String(format: " · ~$%.2f", modelUsage.totalEstimatedCostUSD) : ""
705
+ let summaryItem = NSMenuItem(
706
+ title: "\(totalTokensStr) tokens · \(totalPromptsStr)\(totalCostStr)",
707
+ action: nil, keyEquivalent: "")
708
+ summaryItem.isEnabled = false
709
+ let summaryFont = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)
710
+ summaryItem.attributedTitle = NSAttributedString(
711
+ string: summaryItem.title,
712
+ attributes: [.font: summaryFont, .foregroundColor: NSColor.secondaryLabelColor])
713
+ submenu.addItem(summaryItem)
714
+ submenu.addItem(.separator())
715
+
716
+ for entry in modelUsage.entries {
717
+ let tokens = UsageFormatter.tokenCountString(entry.tokens)
718
+ var title = "\(entry.modelCode): \(tokens)"
719
+ if entry.prompts > 0 {
720
+ title += " · \(entry.prompts)p"
721
+ }
722
+ if let cost = entry.estimatedCostUSD, cost > 0.001 {
723
+ title += String(format: " · ~$%.3f", cost)
724
+ }
725
+ let item = NSMenuItem(title: title, action: nil, keyEquivalent: "")
726
+ submenu.addItem(item)
727
+ }
728
+
729
+ if hasToolUsage || hasMCPDetails {
730
+ submenu.addItem(.separator())
731
+ }
732
+ }
733
+
734
+ // MARK: Tool usage (24h) — MCP tool call counts
735
+ if let toolUsage = zai.toolUsage, !toolUsage.entries.isEmpty {
736
+ let headerFont = NSFont.systemFont(ofSize: NSFont.systemFontSize, weight: .semibold)
737
+ let header = NSMenuItem(title: "MCP Tools (24h)", action: nil, keyEquivalent: "")
738
+ header.attributedTitle = NSAttributedString(string: "MCP Tools (24h)", attributes: [.font: headerFont])
739
+ header.isEnabled = false
740
+ submenu.addItem(header)
741
+
742
+ let totalItem = NSMenuItem(
743
+ title: "\(toolUsage.totalCalls) total calls",
744
+ action: nil, keyEquivalent: "")
745
+ totalItem.isEnabled = false
746
+ let totalFont = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)
747
+ totalItem.attributedTitle = NSAttributedString(
748
+ string: totalItem.title,
749
+ attributes: [.font: totalFont, .foregroundColor: NSColor.secondaryLabelColor])
750
+ submenu.addItem(totalItem)
751
+ submenu.addItem(.separator())
752
+
753
+ for entry in toolUsage.entries {
754
+ let displayName = self.displayToolName(entry.toolName)
755
+ let item = NSMenuItem(
756
+ title: "\(displayName): \(entry.count) calls",
757
+ action: nil, keyEquivalent: "")
758
+ submenu.addItem(item)
759
+ }
760
+
761
+ if hasMCPDetails {
762
+ submenu.addItem(.separator())
763
+ }
764
+ }
765
+
766
+ // MARK: Quota MCP details (existing — from timeLimit.usageDetails)
767
+ if let timeLimit = zai.timeLimit, !timeLimit.usageDetails.isEmpty {
768
+ let headerFont = NSFont.systemFont(ofSize: NSFont.systemFontSize, weight: .semibold)
769
+ let header = NSMenuItem(title: "Quota Window", action: nil, keyEquivalent: "")
770
+ header.attributedTitle = NSAttributedString(string: "Quota Window", attributes: [.font: headerFont])
771
+ header.isEnabled = false
772
+ submenu.addItem(header)
773
+
774
+ if let window = timeLimit.windowLabel {
775
+ let item = NSMenuItem(title: window, action: nil, keyEquivalent: "")
776
+ item.isEnabled = false
777
+ submenu.addItem(item)
778
+ }
779
+ if let resetTime = timeLimit.nextResetTime {
780
+ let reset = UsageFormatter.resetDescription(from: resetTime)
781
+ let item = NSMenuItem(title: "Resets: \(reset)", action: nil, keyEquivalent: "")
782
+ item.isEnabled = false
783
+ submenu.addItem(item)
784
+ }
785
+ submenu.addItem(.separator())
786
+
787
+ let sortedDetails = timeLimit.usageDetails.sorted {
788
+ $0.modelCode.localizedCaseInsensitiveCompare($1.modelCode) == .orderedAscending
789
+ }
790
+ for detail in sortedDetails {
791
+ let usage = UsageFormatter.tokenCountString(detail.usage)
792
+ let item = NSMenuItem(title: "\(detail.modelCode): \(usage)", action: nil, keyEquivalent: "")
793
+ submenu.addItem(item)
794
+ }
795
+ }
796
+
797
+ return submenu
798
+ }
799
+
800
+ private func displayToolName(_ raw: String) -> String {
801
+ let mapping: [String: String] = [
802
+ "web_search": "Web Search",
803
+ "web_reader": "Web Reader",
804
+ "zread": "ZRead",
805
+ "web-search": "Web Search",
806
+ "web-reader": "Web Reader",
807
+ ]
808
+ return mapping[raw.lowercased()] ?? raw
809
+ }
810
+ }