@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.
- package/CHANGELOG.md +256 -0
- package/LICENSE +373 -21
- package/README.md +96 -89
- package/THIRD-PARTY-NOTICES.md +651 -0
- package/package.json +14 -4
- package/.swiftformat +0 -50
- package/.swiftlint.yml +0 -160
- package/CONTRIBUTING.md +0 -456
- package/Icon.icns +0 -0
- package/Icon.icon/Assets/runic.png +0 -0
- package/Icon.icon/icon.json +0 -36
- package/Package.resolved +0 -51
- package/Package.swift +0 -136
- package/Packages/Helix/LICENSE +0 -21
- package/Packages/Helix/Package.swift +0 -29
- package/Packages/Helix/README.md +0 -976
- package/Packages/Helix/Sources/Helix/Binding.swift +0 -111
- package/Packages/Helix/Sources/Helix/CommandParser.swift +0 -160
- package/Packages/Helix/Sources/Helix/CommandSignature.swift +0 -117
- package/Packages/Helix/Sources/Helix/ExpressibleFromArgument.swift +0 -72
- package/Packages/Helix/Sources/Helix/HelpGenerator.swift +0 -175
- package/Packages/Helix/Sources/Helix/NameSpecification.swift +0 -98
- package/Packages/Helix/Sources/Helix/ParsableCommand.swift +0 -204
- package/Packages/Helix/Sources/Helix/Platform/DarwinPlatformContext.swift +0 -44
- package/Packages/Helix/Sources/Helix/Platform/LinuxPlatformContext.swift +0 -34
- package/Packages/Helix/Sources/Helix/Platform/PlatformContext+Default.swift +0 -91
- package/Packages/Helix/Sources/Helix/Platform/PlatformContext.swift +0 -94
- package/Packages/Helix/Sources/Helix/Platform/PlatformPath.swift +0 -167
- package/Packages/Helix/Sources/Helix/Platform/Stdio.swift +0 -99
- package/Packages/Helix/Sources/Helix/Platform/WebPlatformContext.swift +0 -146
- package/Packages/Helix/Sources/Helix/Platform/WindowsPlatformContext.swift +0 -97
- package/Packages/Helix/Sources/Helix/Program.swift +0 -204
- package/Packages/Helix/Sources/Helix/PropertyWrappers.swift +0 -367
- package/Packages/Silo/LICENSE +0 -21
- package/Packages/Silo/Package.swift +0 -34
- package/Packages/Silo/README.md +0 -466
- package/Packages/Silo/Sources/Silo/BinaryCookiesReader.swift +0 -169
- package/Packages/Silo/Sources/Silo/BrowserCookieAnalytics.swift +0 -365
- package/Packages/Silo/Sources/Silo/BrowserCookieBackends.swift +0 -594
- package/Packages/Silo/Sources/Silo/BrowserCookieClient.swift +0 -326
- package/Packages/Silo/Sources/Silo/BrowserCookieExport.swift +0 -154
- package/Packages/Silo/Sources/Silo/BrowserCookieImport.swift +0 -119
- package/Packages/Silo/Sources/Silo/BrowserCookieMockStore.swift +0 -121
- package/Packages/Silo/Sources/Silo/BrowserCookieModels.swift +0 -391
- package/Packages/Silo/Sources/Silo/ChromiumCookieReader.swift +0 -240
- package/Packages/Silo/Sources/Silo/CryptoSupport.swift +0 -86
- package/Packages/Silo/Sources/Silo/FirefoxCookieReader.swift +0 -172
- package/Packages/Silo/Sources/Silo/LinuxChromiumCookieReader.swift +0 -172
- package/Packages/Silo/Sources/Silo/LinuxSecretService.swift +0 -105
- package/Packages/Silo/Sources/Silo/MacOSChromiumCookieReader.swift +0 -191
- package/Packages/Silo/Sources/Silo/SQLiteSnapshot.swift +0 -41
- package/Packages/Silo/Sources/Silo/WindowsChromiumCookieReader.swift +0 -110
- package/RuniciOS/App/RuniciOSApp.swift +0 -105
- package/RuniciOS/Core/RunicTypographyiOS.swift +0 -24
- package/RuniciOS/Core/iOSUsageStore.swift +0 -192
- package/RuniciOS/Views/ProviderDetailView.swift +0 -312
- package/RuniciOS/Views/ProviderListView.swift +0 -228
- package/RuniciOS/Widgets/LargeWidget.swift +0 -367
- package/RuniciOS/Widgets/LiveActivity.swift +0 -342
- package/RuniciOS/Widgets/LockScreenWidget.swift +0 -273
- package/RuniciOS/Widgets/MediumWidget.swift +0 -370
- package/RuniciOS/Widgets/RunicWidget.swift +0 -274
- package/RuniciOS/Widgets/SmallWidget.swift +0 -281
- package/RuniciOS/Widgets/WidgetModels.swift +0 -285
- package/RuniciOS/Widgets/WidgetTimeline.swift +0 -270
- package/Scripts/CREATION-SUMMARY.md +0 -367
- package/Scripts/DIRECTORY-STRUCTURE.md +0 -358
- package/Scripts/GETTING-STARTED-CHECKLIST.md +0 -364
- package/Scripts/INDEX.md +0 -365
- package/Scripts/QUICK-REFERENCE.md +0 -164
- package/Scripts/README-SCRIPTS.md +0 -926
- package/Scripts/build-android.sh +0 -526
- package/Scripts/build-ios.sh +0 -529
- package/Scripts/build-macos.sh +0 -603
- package/Scripts/build-windows.sh +0 -504
- package/Scripts/build_icon.sh +0 -49
- package/Scripts/changelog-to-html.sh +0 -89
- package/Scripts/check-release-assets.sh +0 -10
- package/Scripts/ci-config.yml +0 -474
- package/Scripts/compile_and_run.sh +0 -219
- package/Scripts/compile_and_run_adhoc.sh +0 -4
- package/Scripts/docs-list.mjs +0 -119
- package/Scripts/make_appcast.sh +0 -58
- package/Scripts/package_app.sh +0 -409
- package/Scripts/release-lib.sh +0 -214
- package/Scripts/release.sh +0 -76
- package/Scripts/setup-all.sh +0 -387
- package/Scripts/setup-ios.sh +0 -404
- package/Scripts/setup-react-native.sh +0 -523
- package/Scripts/sign-and-notarize.sh +0 -122
- package/Scripts/test_live_update.sh +0 -38
- package/Scripts/validate_changelog.sh +0 -23
- package/Scripts/verify_appcast.sh +0 -92
- package/Sources/Runic/About.swift +0 -93
- package/Sources/Runic/AppNotifications.swift +0 -117
- package/Sources/Runic/Controllers/StatusItemController+Actions.swift +0 -670
- package/Sources/Runic/Controllers/StatusItemController+Animation.swift +0 -519
- package/Sources/Runic/Controllers/StatusItemController+ChartSubmenus.swift +0 -824
- package/Sources/Runic/Controllers/StatusItemController+CustomProviders.swift +0 -90
- package/Sources/Runic/Controllers/StatusItemController+Menu.swift +0 -1412
- package/Sources/Runic/Controllers/StatusItemController+MenuCards.swift +0 -339
- package/Sources/Runic/Controllers/StatusItemController+MenuHelpers.swift +0 -423
- package/Sources/Runic/Controllers/StatusItemController.swift +0 -360
- package/Sources/Runic/Core/BudgetNotificationManager.swift +0 -81
- package/Sources/Runic/Core/Login/ClaudeLoginRunner.swift +0 -103
- package/Sources/Runic/Core/Login/CodexLoginRunner.swift +0 -154
- package/Sources/Runic/Core/Login/CursorLoginRunner.swift +0 -206
- package/Sources/Runic/Core/Login/GeminiLoginRunner.swift +0 -92
- package/Sources/Runic/Core/Rendering/IconRenderer.swift +0 -1222
- package/Sources/Runic/Core/Rendering/IconView.swift +0 -122
- package/Sources/Runic/Core/Rendering/ProviderBrandIcon.swift +0 -71
- package/Sources/Runic/Core/RunicAnimation.swift +0 -53
- package/Sources/Runic/Core/RunicColors.swift +0 -73
- package/Sources/Runic/Core/RunicCornerRadius.swift +0 -15
- package/Sources/Runic/Core/RunicSpacing.swift +0 -68
- package/Sources/Runic/Core/RunicTypography.swift +0 -206
- package/Sources/Runic/Core/Stores/AuggieTokenStore.swift +0 -117
- package/Sources/Runic/Core/Stores/AzureOpenAITokenStore.swift +0 -117
- package/Sources/Runic/Core/Stores/CerebrasTokenStore.swift +0 -117
- package/Sources/Runic/Core/Stores/CohereTokenStore.swift +0 -117
- package/Sources/Runic/Core/Stores/CopilotTokenStore.swift +0 -117
- package/Sources/Runic/Core/Stores/CustomProviderStore.swift +0 -158
- package/Sources/Runic/Core/Stores/DeepSeekTokenStore.swift +0 -117
- package/Sources/Runic/Core/Stores/FireworksTokenStore.swift +0 -117
- package/Sources/Runic/Core/Stores/GroqTokenStore.swift +0 -117
- package/Sources/Runic/Core/Stores/KimiTokenStore.swift +0 -117
- package/Sources/Runic/Core/Stores/MiniMaxCookieHeaderStore.swift +0 -117
- package/Sources/Runic/Core/Stores/MiniMaxGroupIDStore.swift +0 -117
- package/Sources/Runic/Core/Stores/MiniMaxTokenStore.swift +0 -117
- package/Sources/Runic/Core/Stores/MistralTokenStore.swift +0 -117
- package/Sources/Runic/Core/Stores/OpenRouterTokenStore.swift +0 -117
- package/Sources/Runic/Core/Stores/PerplexityTokenStore.swift +0 -117
- package/Sources/Runic/Core/Stores/ProviderToggleStore.swift +0 -30
- package/Sources/Runic/Core/Stores/QwenTokenStore.swift +0 -117
- package/Sources/Runic/Core/Stores/SambaNovaTokenStore.swift +0 -117
- package/Sources/Runic/Core/Stores/SettingsStore.swift +0 -1749
- package/Sources/Runic/Core/Stores/TogetherTokenStore.swift +0 -117
- package/Sources/Runic/Core/Stores/UsageStore+Accessors.swift +0 -33
- package/Sources/Runic/Core/Stores/UsageStore+Status.swift +0 -189
- package/Sources/Runic/Core/Stores/UsageStore+TokenCost.swift +0 -33
- package/Sources/Runic/Core/Stores/UsageStore+WidgetSnapshot.swift +0 -67
- package/Sources/Runic/Core/Stores/UsageStore.swift +0 -3631
- package/Sources/Runic/Core/Stores/XAITokenStore.swift +0 -117
- package/Sources/Runic/Core/Stores/ZaiTokenStore.swift +0 -117
- package/Sources/Runic/Core/UsageExporter.swift +0 -197
- package/Sources/Runic/Date+RelativeDescription.swift +0 -21
- package/Sources/Runic/HiddenWindowView.swift +0 -32
- package/Sources/Runic/InstallOrigin.swift +0 -9
- package/Sources/Runic/KeyboardShortcuts+Names.swift +0 -6
- package/Sources/Runic/MouseLocationReader.swift +0 -65
- package/Sources/Runic/Notifications+Runic.swift +0 -6
- package/Sources/Runic/OpenAICreditsPurchaseWindowController.swift +0 -503
- package/Sources/Runic/ProviderRegistry.swift +0 -100
- package/Sources/Runic/ProviderSwitcherButtons.swift +0 -267
- package/Sources/Runic/Providers/Antigravity/AntigravityLoginFlow.swift +0 -11
- package/Sources/Runic/Providers/Antigravity/AntigravityProviderImplementation.swift +0 -14
- package/Sources/Runic/Providers/Auggie/AuggieProviderImplementation.swift +0 -24
- package/Sources/Runic/Providers/Azure/AzureProviderImplementation.swift +0 -52
- package/Sources/Runic/Providers/Bedrock/BedrockProviderImplementation.swift +0 -42
- package/Sources/Runic/Providers/Cerebras/CerebrasProviderImplementation.swift +0 -24
- package/Sources/Runic/Providers/Claude/ClaudeLoginFlow.swift +0 -26
- package/Sources/Runic/Providers/Claude/ClaudeProviderImplementation.swift +0 -14
- package/Sources/Runic/Providers/Codex/CodexLoginFlow.swift +0 -18
- package/Sources/Runic/Providers/Codex/CodexProviderImplementation.swift +0 -15
- package/Sources/Runic/Providers/Cohere/CohereProviderImplementation.swift +0 -24
- package/Sources/Runic/Providers/Copilot/CopilotLoginFlow.swift +0 -193
- package/Sources/Runic/Providers/Copilot/CopilotProviderImplementation.swift +0 -110
- package/Sources/Runic/Providers/Cursor/CursorLoginFlow.swift +0 -28
- package/Sources/Runic/Providers/Cursor/CursorProviderImplementation.swift +0 -15
- package/Sources/Runic/Providers/DeepSeek/DeepSeekProviderImplementation.swift +0 -24
- package/Sources/Runic/Providers/Factory/FactoryLoginFlow.swift +0 -12
- package/Sources/Runic/Providers/Factory/FactoryProviderImplementation.swift +0 -15
- package/Sources/Runic/Providers/Fireworks/FireworksProviderImplementation.swift +0 -24
- package/Sources/Runic/Providers/Gemini/GeminiLoginFlow.swift +0 -20
- package/Sources/Runic/Providers/Gemini/GeminiProviderImplementation.swift +0 -15
- package/Sources/Runic/Providers/Groq/GroqProviderImplementation.swift +0 -24
- package/Sources/Runic/Providers/Kimi/KimiProviderImplementation.swift +0 -24
- package/Sources/Runic/Providers/MiniMax/MiniMaxProviderImplementation.swift +0 -32
- package/Sources/Runic/Providers/Mistral/MistralProviderImplementation.swift +0 -24
- package/Sources/Runic/Providers/OpenRouter/OpenRouterProviderImplementation.swift +0 -24
- package/Sources/Runic/Providers/Perplexity/PerplexityProviderImplementation.swift +0 -24
- package/Sources/Runic/Providers/SambaNova/SambaNovaProviderImplementation.swift +0 -24
- package/Sources/Runic/Providers/Shared/ProviderCatalog.swift +0 -15
- package/Sources/Runic/Providers/Shared/ProviderImplementation.swift +0 -51
- package/Sources/Runic/Providers/Shared/ProviderImplementationRegistry.swift +0 -66
- package/Sources/Runic/Providers/Shared/ProviderLoginFlow.swift +0 -12
- package/Sources/Runic/Providers/Shared/ProviderSettingsDescriptors.swift +0 -99
- package/Sources/Runic/Providers/Shared/SystemSettingsLinks.swift +0 -18
- package/Sources/Runic/Providers/Together/TogetherProviderImplementation.swift +0 -24
- package/Sources/Runic/Providers/XAI/XAIProviderImplementation.swift +0 -24
- package/Sources/Runic/Providers/Zai/ZaiProviderImplementation.swift +0 -24
- package/Sources/Runic/Resources/Fonts/Caveat-Bold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/Caveat-Regular.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/FiraCode-Bold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/FiraCode-Light.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/FiraCode-Medium.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/FiraCode-Regular.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/FiraCode-SemiBold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/IBMPlexMono-Bold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/IBMPlexMono-Medium.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/IBMPlexMono-Regular.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/IBMPlexMono-SemiBold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/Inconsolata-Bold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/Inconsolata-Medium.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/Inconsolata-Regular.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/Inconsolata-SemiBold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/JetBrainsMono-Bold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/JetBrainsMono-Medium.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/JetBrainsMono-Regular.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/JetBrainsMono-SemiBold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/SpaceMono-Bold.ttf +0 -0
- package/Sources/Runic/Resources/Fonts/SpaceMono-Regular.ttf +0 -0
- package/Sources/Runic/Resources/ProviderIcon-antigravity.svg +0 -2
- package/Sources/Runic/Resources/ProviderIcon-auggie.svg +0 -3
- package/Sources/Runic/Resources/ProviderIcon-azure.svg +0 -5
- package/Sources/Runic/Resources/ProviderIcon-bedrock.svg +0 -7
- package/Sources/Runic/Resources/ProviderIcon-cerebras.svg +0 -3
- package/Sources/Runic/Resources/ProviderIcon-claude.svg +0 -1
- package/Sources/Runic/Resources/ProviderIcon-codex.svg +0 -1
- package/Sources/Runic/Resources/ProviderIcon-cohere.svg +0 -3
- package/Sources/Runic/Resources/ProviderIcon-copilot.svg +0 -1
- package/Sources/Runic/Resources/ProviderIcon-cursor.svg +0 -3
- package/Sources/Runic/Resources/ProviderIcon-deepseek.svg +0 -4
- package/Sources/Runic/Resources/ProviderIcon-factory.svg +0 -3
- package/Sources/Runic/Resources/ProviderIcon-fireworks.svg +0 -3
- package/Sources/Runic/Resources/ProviderIcon-gemini.svg +0 -1
- package/Sources/Runic/Resources/ProviderIcon-groq.svg +0 -1
- package/Sources/Runic/Resources/ProviderIcon-kimi.svg +0 -4
- package/Sources/Runic/Resources/ProviderIcon-minimax.svg +0 -1
- package/Sources/Runic/Resources/ProviderIcon-mistral.svg +0 -5
- package/Sources/Runic/Resources/ProviderIcon-openrouter.svg +0 -7
- package/Sources/Runic/Resources/ProviderIcon-perplexity.svg +0 -4
- package/Sources/Runic/Resources/ProviderIcon-qwen.svg +0 -9
- package/Sources/Runic/Resources/ProviderIcon-sambanova.svg +0 -4
- package/Sources/Runic/Resources/ProviderIcon-together.svg +0 -5
- package/Sources/Runic/Resources/ProviderIcon-vertexai.svg +0 -17
- package/Sources/Runic/Resources/ProviderIcon-xai.svg +0 -3
- package/Sources/Runic/Resources/ProviderIcon-zai.svg +0 -5
- package/Sources/Runic/Resources/RunicMenubarIcon.svg +0 -7
- package/Sources/Runic/Resources/provider-context-windows.json +0 -20
- package/Sources/Runic/RunicApp.swift +0 -367
- package/Sources/Runic/SessionQuotaNotifications.swift +0 -58
- package/Sources/Runic/UsagePaceText.swift +0 -52
- package/Sources/Runic/UsageProgressBar.swift +0 -228
- package/Sources/Runic/Utilities/DisplayLink.swift +0 -96
- package/Sources/Runic/Utilities/LoadingPattern.swift +0 -62
- package/Sources/Runic/Utilities/PerformanceConstants.swift +0 -85
- package/Sources/Runic/Views/Menu/AlertsMenuView.swift +0 -254
- package/Sources/Runic/Views/Menu/ChartPanelStyle.swift +0 -22
- package/Sources/Runic/Views/Menu/CostComparativeChartMenuView.swift +0 -275
- package/Sources/Runic/Views/Menu/CostHistoryChartMenuView.swift +0 -377
- package/Sources/Runic/Views/Menu/CreditsHistoryChartMenuView.swift +0 -312
- package/Sources/Runic/Views/Menu/EfficiencyMetricsMenuView.swift +0 -311
- package/Sources/Runic/Views/Menu/HeroStatCardsMenuView.swift +0 -231
- package/Sources/Runic/Views/Menu/HourlyActivityChartMenuView.swift +0 -228
- package/Sources/Runic/Views/Menu/InlineUsageChartView.swift +0 -157
- package/Sources/Runic/Views/Menu/MenuCardEntranceModifier.swift +0 -63
- package/Sources/Runic/Views/Menu/MenuCardSidebarView.swift +0 -248
- package/Sources/Runic/Views/Menu/MenuCardSkeletonView.swift +0 -104
- package/Sources/Runic/Views/Menu/MenuCardView.swift +0 -1889
- package/Sources/Runic/Views/Menu/MenuContent.swift +0 -153
- package/Sources/Runic/Views/Menu/MenuDescriptor.swift +0 -371
- package/Sources/Runic/Views/Menu/MenuHighlightStyle.swift +0 -35
- package/Sources/Runic/Views/Menu/ModelBreakdownMenuView.swift +0 -241
- package/Sources/Runic/Views/Menu/OverviewMenuView.swift +0 -281
- package/Sources/Runic/Views/Menu/ProjectBreakdownMenuView.swift +0 -214
- package/Sources/Runic/Views/Menu/ProjectBudgetMenuView.swift +0 -220
- package/Sources/Runic/Views/Menu/ProviderTabBarView.swift +0 -86
- package/Sources/Runic/Views/Menu/SubscriptionUtilizationChartMenuView.swift +0 -214
- package/Sources/Runic/Views/Menu/UsageBreakdownChartMenuView.swift +0 -398
- package/Sources/Runic/Views/Menu/UsageHeatmapMenuView.swift +0 -231
- package/Sources/Runic/Views/Menu/UsageTimelineChartMenuView.swift +0 -369
- package/Sources/Runic/Views/Menu/UsageWindowComparisonChartMenuView.swift +0 -191
- package/Sources/Runic/Views/Menu/WeeklyActivityChartMenuView.swift +0 -132
- package/Sources/Runic/Views/Performance/PerformanceDashboardView.swift +0 -621
- package/Sources/Runic/Views/Preferences/LiquidComponents.swift +0 -306
- package/Sources/Runic/Views/Preferences/PreferencesAboutPane.swift +0 -271
- package/Sources/Runic/Views/Preferences/PreferencesAdvancedPane.swift +0 -268
- package/Sources/Runic/Views/Preferences/PreferencesAlertsPane.swift +0 -418
- package/Sources/Runic/Views/Preferences/PreferencesAnalyticsPane.swift +0 -637
- package/Sources/Runic/Views/Preferences/PreferencesBudgetsPane.swift +0 -344
- package/Sources/Runic/Views/Preferences/PreferencesComponents.swift +0 -230
- package/Sources/Runic/Views/Preferences/PreferencesCustomProvidersPane.swift +0 -321
- package/Sources/Runic/Views/Preferences/PreferencesDebugPane.swift +0 -469
- package/Sources/Runic/Views/Preferences/PreferencesGeneralPane.swift +0 -250
- package/Sources/Runic/Views/Preferences/PreferencesHelpPane.swift +0 -50
- package/Sources/Runic/Views/Preferences/PreferencesIntegrationsPane.swift +0 -572
- package/Sources/Runic/Views/Preferences/PreferencesPerformancePane.swift +0 -291
- package/Sources/Runic/Views/Preferences/PreferencesProvidersPane.swift +0 -3480
- package/Sources/Runic/Views/Preferences/PreferencesSelection.swift +0 -29
- package/Sources/Runic/Views/Preferences/PreferencesSyncPane.swift +0 -61
- package/Sources/Runic/Views/Preferences/PreferencesView.swift +0 -213
- package/Sources/Runic/Views/Rating/QualityRatingView.swift +0 -238
- package/Sources/Runic/Views/Team/TeamDashboardView.swift +0 -376
- package/Sources/Runic/Views/Team/TeamInviteSheet.swift +0 -175
- package/Sources/Runic/Views/Team/TeamManagementView.swift +0 -762
- package/Sources/Runic/Views/Team/TeamMemberRow.swift +0 -181
- package/Sources/RunicCLI/CLIEntry.swift +0 -983
- package/Sources/RunicCLI/CLIRenderer.swift +0 -87
- package/Sources/RunicCLI/Commands/EnhancedUsageCommand.swift +0 -588
- package/Sources/RunicCLI/Commands/ModelsCommand.swift +0 -363
- package/Sources/RunicCLI/Commands/ProjectsCommand.swift +0 -453
- package/Sources/RunicCLI/Commands/ResetCommand.swift +0 -430
- package/Sources/RunicCLI/Commands/UsageLedgerSourceFactory.swift +0 -167
- package/Sources/RunicClaudeWatchdog/main.swift +0 -122
- package/Sources/RunicClaudeWebProbe/main.swift +0 -62
- package/Sources/RunicCore/AlertRuleStore.swift +0 -187
- package/Sources/RunicCore/BrowserCookieImportOrder.swift +0 -25
- package/Sources/RunicCore/CopilotUsageModels.swift +0 -84
- package/Sources/RunicCore/CostUsageFetcher.swift +0 -119
- package/Sources/RunicCore/CostUsageModels.swift +0 -479
- package/Sources/RunicCore/CreditsModels.swift +0 -50
- package/Sources/RunicCore/CustomProviders/CustomProviderModels.swift +0 -268
- package/Sources/RunicCore/CustomProviders/GenericProviderFetcher.swift +0 -631
- package/Sources/RunicCore/CustomProviders/ProviderTemplates.swift +0 -444
- package/Sources/RunicCore/Host/PTY/TTYCommandRunner.swift +0 -679
- package/Sources/RunicCore/Host/Process/SubprocessRunner.swift +0 -135
- package/Sources/RunicCore/Integrations/GitHubIntegration.swift +0 -173
- package/Sources/RunicCore/Logging/JSONStderrLogHandler.swift +0 -79
- package/Sources/RunicCore/Logging/OSLogLogHandler.swift +0 -86
- package/Sources/RunicCore/Logging/RunicLog.swift +0 -123
- package/Sources/RunicCore/Models/EnhancedUsageModels.swift +0 -370
- package/Sources/RunicCore/OpenAIDashboardModels.swift +0 -210
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardBrowserCookieImporter.swift +0 -676
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardFetcher.swift +0 -492
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardNavigationDelegate.swift +0 -51
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardParser.swift +0 -483
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardScrapeScript.swift +0 -558
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardWebViewCache.swift +0 -204
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardWebsiteDataStore.swift +0 -78
- package/Sources/RunicCore/PathEnvironment.swift +0 -482
- package/Sources/RunicCore/Performance/PerformanceModels.swift +0 -371
- package/Sources/RunicCore/Performance/PerformanceStorageImpl.swift +0 -426
- package/Sources/RunicCore/Policy/UsagePolicyEngine.swift +0 -178
- package/Sources/RunicCore/ProjectBudgetStore.swift +0 -102
- package/Sources/RunicCore/ProviderCostSnapshot.swift +0 -29
- package/Sources/RunicCore/Providers/Antigravity/AntigravityProviderDescriptor.swift +0 -68
- package/Sources/RunicCore/Providers/Antigravity/AntigravityStatusProbe.swift +0 -685
- package/Sources/RunicCore/Providers/Auggie/AuggieProviderDescriptor.swift +0 -83
- package/Sources/RunicCore/Providers/Auggie/AuggieUsageFetcher.swift +0 -142
- package/Sources/RunicCore/Providers/Azure/AzureProviderDescriptor.swift +0 -141
- package/Sources/RunicCore/Providers/Azure/AzureUsageFetcher.swift +0 -188
- package/Sources/RunicCore/Providers/Bedrock/BedrockProviderDescriptor.swift +0 -126
- package/Sources/RunicCore/Providers/Bedrock/BedrockUsageFetcher.swift +0 -164
- package/Sources/RunicCore/Providers/Cerebras/CerebrasProviderDescriptor.swift +0 -84
- package/Sources/RunicCore/Providers/Cerebras/CerebrasUsageFetcher.swift +0 -75
- package/Sources/RunicCore/Providers/Claude/ClaudeCLISession.swift +0 -332
- package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthCredentials.swift +0 -180
- package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthUsageFetcher.swift +0 -137
- package/Sources/RunicCore/Providers/Claude/ClaudeProviderDescriptor.swift +0 -209
- package/Sources/RunicCore/Providers/Claude/ClaudeStatusProbe.swift +0 -654
- package/Sources/RunicCore/Providers/Claude/ClaudeUsageDataSource.swift +0 -30
- package/Sources/RunicCore/Providers/Claude/ClaudeUsageFetcher.swift +0 -480
- package/Sources/RunicCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift +0 -780
- package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthCredentials.swift +0 -147
- package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift +0 -280
- package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexTokenRefresher.swift +0 -100
- package/Sources/RunicCore/Providers/Codex/CodexProviderDescriptor.swift +0 -245
- package/Sources/RunicCore/Providers/Codex/CodexStatusProbe.swift +0 -125
- package/Sources/RunicCore/Providers/Codex/CodexUsageDataSource.swift +0 -26
- package/Sources/RunicCore/Providers/Codex/CodexWebDashboardStrategy.swift +0 -183
- package/Sources/RunicCore/Providers/Cohere/CohereProviderDescriptor.swift +0 -83
- package/Sources/RunicCore/Providers/Cohere/CohereUsageFetcher.swift +0 -85
- package/Sources/RunicCore/Providers/Copilot/CopilotDeviceFlow.swift +0 -123
- package/Sources/RunicCore/Providers/Copilot/CopilotGitHubCLITokenReader.swift +0 -117
- package/Sources/RunicCore/Providers/Copilot/CopilotProviderDescriptor.swift +0 -100
- package/Sources/RunicCore/Providers/Copilot/CopilotUsageFetcher.swift +0 -293
- package/Sources/RunicCore/Providers/Copilot/CopilotVSCodeTokenReader.swift +0 -385
- package/Sources/RunicCore/Providers/Cursor/CursorProviderDescriptor.swift +0 -67
- package/Sources/RunicCore/Providers/Cursor/CursorStatusProbe.swift +0 -595
- package/Sources/RunicCore/Providers/DeepSeek/DeepSeekProviderDescriptor.swift +0 -85
- package/Sources/RunicCore/Providers/DeepSeek/DeepSeekUsageFetcher.swift +0 -108
- package/Sources/RunicCore/Providers/Factory/FactoryLocalStorageImporter.swift +0 -398
- package/Sources/RunicCore/Providers/Factory/FactoryProviderDescriptor.swift +0 -67
- package/Sources/RunicCore/Providers/Factory/FactoryStatusProbe.swift +0 -1276
- package/Sources/RunicCore/Providers/Fireworks/FireworksProviderDescriptor.swift +0 -84
- package/Sources/RunicCore/Providers/Fireworks/FireworksUsageFetcher.swift +0 -75
- package/Sources/RunicCore/Providers/Gemini/GeminiProviderDescriptor.swift +0 -67
- package/Sources/RunicCore/Providers/Gemini/GeminiStatusProbe.swift +0 -839
- package/Sources/RunicCore/Providers/Groq/GroqProviderDescriptor.swift +0 -83
- package/Sources/RunicCore/Providers/Groq/GroqUsageFetcher.swift +0 -71
- package/Sources/RunicCore/Providers/Kimi/KimiProviderDescriptor.swift +0 -83
- package/Sources/RunicCore/Providers/Kimi/KimiUsageFetcher.swift +0 -77
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift +0 -97
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxSettingsReader.swift +0 -54
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxUsageFetcher.swift +0 -168
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxWebUsageFetcher.swift +0 -911
- package/Sources/RunicCore/Providers/Mistral/MistralProviderDescriptor.swift +0 -84
- package/Sources/RunicCore/Providers/Mistral/MistralUsageFetcher.swift +0 -75
- package/Sources/RunicCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift +0 -87
- package/Sources/RunicCore/Providers/OpenRouter/OpenRouterUsageFetcher.swift +0 -216
- package/Sources/RunicCore/Providers/Perplexity/PerplexityProviderDescriptor.swift +0 -84
- package/Sources/RunicCore/Providers/Perplexity/PerplexityUsageFetcher.swift +0 -75
- package/Sources/RunicCore/Providers/ProviderBranding.swift +0 -25
- package/Sources/RunicCore/Providers/ProviderCLIConfig.swift +0 -17
- package/Sources/RunicCore/Providers/ProviderDescriptor.swift +0 -129
- package/Sources/RunicCore/Providers/ProviderFetchPlan.swift +0 -263
- package/Sources/RunicCore/Providers/ProviderSettingsSnapshot.swift +0 -98
- package/Sources/RunicCore/Providers/ProviderTokenResolver.swift +0 -501
- package/Sources/RunicCore/Providers/ProviderVersionDetector.swift +0 -68
- package/Sources/RunicCore/Providers/Providers.swift +0 -184
- package/Sources/RunicCore/Providers/Qwen/QwenProviderDescriptor.swift +0 -86
- package/Sources/RunicCore/Providers/Qwen/QwenSettingsReader.swift +0 -30
- package/Sources/RunicCore/Providers/Qwen/QwenUsageStats.swift +0 -328
- package/Sources/RunicCore/Providers/SambaNova/SambaNovaProviderDescriptor.swift +0 -84
- package/Sources/RunicCore/Providers/SambaNova/SambaNovaUsageFetcher.swift +0 -75
- package/Sources/RunicCore/Providers/Together/TogetherProviderDescriptor.swift +0 -83
- package/Sources/RunicCore/Providers/Together/TogetherUsageFetcher.swift +0 -75
- package/Sources/RunicCore/Providers/VertexAI/VertexAIProviderDescriptor.swift +0 -123
- package/Sources/RunicCore/Providers/VertexAI/VertexAISettingsReader.swift +0 -48
- package/Sources/RunicCore/Providers/VertexAI/VertexAIUsageFetcher.swift +0 -144
- package/Sources/RunicCore/Providers/XAI/XAIProviderDescriptor.swift +0 -83
- package/Sources/RunicCore/Providers/XAI/XAIUsageFetcher.swift +0 -75
- package/Sources/RunicCore/Providers/Zai/ZaiProviderDescriptor.swift +0 -74
- package/Sources/RunicCore/Providers/Zai/ZaiSettingsReader.swift +0 -41
- package/Sources/RunicCore/Providers/Zai/ZaiUsageStats.swift +0 -745
- package/Sources/RunicCore/Sync/BackgroundSyncManager.swift +0 -384
- package/Sources/RunicCore/Sync/SyncConflictResolver.swift +0 -307
- package/Sources/RunicCore/Sync/SyncProtocol.swift +0 -287
- package/Sources/RunicCore/Sync/SyncRecord.swift +0 -402
- package/Sources/RunicCore/Sync/SyncUsageExample.swift +0 -288
- package/Sources/RunicCore/Sync/TeamSyncRecords.swift +0 -768
- package/Sources/RunicCore/Sync/iCloudSyncEngine.swift +0 -338
- package/Sources/RunicCore/Team/TeamModels.swift +0 -584
- package/Sources/RunicCore/Team/TeamShowbackExporter.swift +0 -495
- package/Sources/RunicCore/Team/TeamStore.swift +0 -809
- package/Sources/RunicCore/TextParsing.swift +0 -55
- package/Sources/RunicCore/UsageFetcher.swift +0 -694
- package/Sources/RunicCore/UsageFormatter.swift +0 -410
- package/Sources/RunicCore/UsageLedger/ClaudeUsageLogSource.swift +0 -441
- package/Sources/RunicCore/UsageLedger/CodexUsageLogSource.swift +0 -502
- package/Sources/RunicCore/UsageLedger/LedgerCache.swift +0 -161
- package/Sources/RunicCore/UsageLedger/OTelGenAILedgerAdapter.swift +0 -550
- package/Sources/RunicCore/UsageLedger/UsageLedger.swift +0 -29
- package/Sources/RunicCore/UsageLedger/UsageLedgerAggregator.swift +0 -675
- package/Sources/RunicCore/UsageLedger/UsageLedgerInsightsAdvisor.swift +0 -209
- package/Sources/RunicCore/UsageLedger/UsageLedgerModels.swift +0 -417
- package/Sources/RunicCore/UsageLedger/UsageLedgerProjectIdentity.swift +0 -284
- package/Sources/RunicCore/UsagePace.swift +0 -81
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageCache.swift +0 -73
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageJsonl.swift +0 -74
- package/Sources/RunicCore/Vendored/CostUsage/CostUsagePricing.swift +0 -208
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner+Claude.swift +0 -455
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner+Timestamp.swift +0 -132
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner.swift +0 -589
- package/Sources/RunicCore/WidgetSnapshot.swift +0 -183
- package/Sources/RunicMacroSupport/ProviderRegistrationMacros.swift +0 -14
- package/Sources/RunicMacros/ProviderRegistrationMacros.swift +0 -216
- package/Sources/RunicWidget/RunicWidgetBundle.swift +0 -79
- package/Sources/RunicWidget/RunicWidgetProvider.swift +0 -283
- package/Sources/RunicWidget/RunicWidgetViews.swift +0 -680
- package/Tests/RunicTests/AntigravityStatusProbeTests.swift +0 -50
- package/Tests/RunicTests/AppDelegateTests.swift +0 -48
- package/Tests/RunicTests/BrowserCookieOrderLabelTests.swift +0 -21
- package/Tests/RunicTests/CLIArgumentParsingTests.swift +0 -69
- package/Tests/RunicTests/CLICostTests.swift +0 -64
- package/Tests/RunicTests/CLIProviderSelectionTests.swift +0 -21
- package/Tests/RunicTests/CLISnapshotTests.swift +0 -71
- package/Tests/RunicTests/CLIWebFallbackTests.swift +0 -57
- package/Tests/RunicTests/ClaudeOAuthTests.swift +0 -163
- package/Tests/RunicTests/ClaudeResilienceTests.swift +0 -29
- package/Tests/RunicTests/ClaudeUsageTests.swift +0 -369
- package/Tests/RunicTests/CodexOAuthTests.swift +0 -121
- package/Tests/RunicTests/CodexUsageLogSourceTests.swift +0 -80
- package/Tests/RunicTests/CopilotTokenResolverTests.swift +0 -74
- package/Tests/RunicTests/CopilotVSCodeTokenReaderTests.swift +0 -74
- package/Tests/RunicTests/CostUsageDecodingTests.swift +0 -392
- package/Tests/RunicTests/CostUsagePricingTests.swift +0 -48
- package/Tests/RunicTests/CostUsageScannerTests.swift +0 -698
- package/Tests/RunicTests/CursorStatusProbeTests.swift +0 -322
- package/Tests/RunicTests/FactoryStatusProbeTests.swift +0 -67
- package/Tests/RunicTests/GeminiAPITestHelpers.swift +0 -94
- package/Tests/RunicTests/GeminiLoginAlertTests.swift +0 -27
- package/Tests/RunicTests/GeminiStatusProbeAPITests.swift +0 -283
- package/Tests/RunicTests/GeminiStatusProbePlanTests.swift +0 -279
- package/Tests/RunicTests/GeminiStatusProbeTests.swift +0 -232
- package/Tests/RunicTests/GeminiTestEnvironment.swift +0 -87
- package/Tests/RunicTests/GoogleWorkspaceStatusTests.swift +0 -72
- package/Tests/RunicTests/InstallOriginTests.swift +0 -17
- package/Tests/RunicTests/KeyboardShortcutsBundleTests.swift +0 -9
- package/Tests/RunicTests/LiveAccountTests.swift +0 -24
- package/Tests/RunicTests/LoadingPatternTests.swift +0 -34
- package/Tests/RunicTests/MenuCardModelTests.swift +0 -544
- package/Tests/RunicTests/MiniMaxCookieHeaderStoreTestSupport.swift +0 -11
- package/Tests/RunicTests/MiniMaxGroupIDStoreTestSupport.swift +0 -11
- package/Tests/RunicTests/MiniMaxParsingTests.swift +0 -67
- package/Tests/RunicTests/MiniMaxTokenStoreTestSupport.swift +0 -11
- package/Tests/RunicTests/OTelGenAILedgerAdapterTests.swift +0 -110
- package/Tests/RunicTests/OpenAIDashboardBrowserCookieImporterTests.swift +0 -16
- package/Tests/RunicTests/OpenAIDashboardFetcherCreditsWaitTests.swift +0 -74
- package/Tests/RunicTests/OpenAIDashboardOffscreenHostTests.swift +0 -26
- package/Tests/RunicTests/OpenAIDashboardParserTests.swift +0 -133
- package/Tests/RunicTests/OpenAIWebAccountSwitchTests.swift +0 -64
- package/Tests/RunicTests/OpenRouterTokenStoreTestSupport.swift +0 -131
- package/Tests/RunicTests/PathBuilderTests.swift +0 -236
- package/Tests/RunicTests/ProviderFetchPipelineTests.swift +0 -159
- package/Tests/RunicTests/ProviderIconResourcesTests.swift +0 -37
- package/Tests/RunicTests/ProviderMetadataStatusLinkTests.swift +0 -15
- package/Tests/RunicTests/ProviderRegistryTests.swift +0 -29
- package/Tests/RunicTests/ProviderSettingsDescriptorTests.swift +0 -236
- package/Tests/RunicTests/ProviderToggleStoreTests.swift +0 -50
- package/Tests/RunicTests/RunicTests.swift +0 -119
- package/Tests/RunicTests/SessionQuotaNotificationLogicTests.swift +0 -35
- package/Tests/RunicTests/SettingsStoreTests.swift +0 -265
- package/Tests/RunicTests/StatusItemAnimationTests.swift +0 -54
- package/Tests/RunicTests/StatusMenuTests.swift +0 -475
- package/Tests/RunicTests/StatusProbeTests.swift +0 -376
- package/Tests/RunicTests/SubprocessRunnerTests.swift +0 -18
- package/Tests/RunicTests/SubscriptionDetectionTests.swift +0 -64
- package/Tests/RunicTests/TTYCommandRunnerTests.swift +0 -147
- package/Tests/RunicTests/TTYIntegrationTests.swift +0 -41
- package/Tests/RunicTests/TeamShowbackExporterTests.swift +0 -157
- package/Tests/RunicTests/TextParsingTests.swift +0 -11
- package/Tests/RunicTests/UsageFormatterTests.swift +0 -111
- package/Tests/RunicTests/UsageLedgerAnomalyDetectionTests.swift +0 -170
- package/Tests/RunicTests/UsageLedgerForecastTests.swift +0 -211
- package/Tests/RunicTests/UsageLedgerInsightsAdvisorTests.swift +0 -81
- package/Tests/RunicTests/UsageLedgerProjectNameTests.swift +0 -168
- package/Tests/RunicTests/UsagePaceTests.swift +0 -76
- package/Tests/RunicTests/UsagePaceTextTests.swift +0 -108
- package/Tests/RunicTests/UsagePolicyEngineTests.swift +0 -89
- package/Tests/RunicTests/WidgetSnapshotTests.swift +0 -43
- package/Tests/RunicTests/ZaiProviderTests.swift +0 -59
- package/Tests/RunicTests/ZaiTokenStoreTestSupport.swift +0 -9
- package/TestsLinux/PlatformGatingTests.swift +0 -46
- package/appcast.xml +0 -104
- package/assets/logo-concept-stack.svg +0 -24
- package/assets/logo-minimal-bars.svg +0 -15
- package/assets/logo.svg +0 -20
- package/assets/logo2.svg +0 -43
- package/bin/docs-list +0 -5
- package/build-and-run.sh +0 -369
- package/docs/.nojekyll +0 -1
- package/docs/CNAME +0 -1
- package/docs/antigravity.md +0 -75
- package/docs/architecture.md +0 -32
- package/docs/claude.md +0 -93
- package/docs/cli-commands/integration_guide.md +0 -371
- package/docs/cli-commands/quick_reference.md +0 -362
- package/docs/cli-commands/readme.md +0 -361
- package/docs/cli.md +0 -156
- package/docs/cloudkit_sync_guide.md +0 -495
- package/docs/codebase_review.md +0 -288
- package/docs/codex-oauth.md +0 -635
- package/docs/codex.md +0 -104
- package/docs/codexbar.png +0 -0
- package/docs/copilot.md +0 -44
- package/docs/cursor.md +0 -46
- package/docs/design/designcomparisonchart.md +0 -510
- package/docs/design/implementationguide.md +0 -792
- package/docs/design/menubaricondesignconcepts.md +0 -796
- package/docs/design/mockups/hybridiconconcept.svg +0 -62
- package/docs/design/mockups/infinityiconconcept.svg +0 -41
- package/docs/design/mockups/infinityiconstates.svg +0 -129
- package/docs/design/mockups/wavelogorefined.svg +0 -48
- package/docs/design/quick_summary.md +0 -426
- package/docs/design/readme.md +0 -478
- package/docs/design/sizespecifications.md +0 -575
- package/docs/design-system.md +0 -836
- package/docs/factory.md +0 -103
- package/docs/gemini.md +0 -73
- package/docs/icon.md +0 -37
- package/docs/icon.png +0 -0
- package/docs/index.html +0 -175
- package/docs/keychain_fix.md +0 -274
- package/docs/minimax.md +0 -71
- package/docs/packaging.md +0 -25
- package/docs/progress-bar-improvements.md +0 -240
- package/docs/progress-bar-quick-reference.md +0 -283
- package/docs/progress-bar-technical-details.md +0 -432
- package/docs/progress-bar-visual-test.swift +0 -375
- package/docs/provider.md +0 -202
- package/docs/providers.md +0 -86
- package/docs/refactor/macros.md +0 -35
- package/docs/refresh-loop.md +0 -22
- package/docs/releasing-homebrew.md +0 -35
- package/docs/releasing.md +0 -121
- package/docs/research/concrete_todo_2026-02-22.md +0 -47
- package/docs/research/core-feature-research.md +0 -248
- package/docs/research/feature_uiux_roadmap_2026-02-22.md +0 -202
- package/docs/research/parity_matrix.md +0 -49
- package/docs/research/provider_expansion_candidates_2026-02-22.md +0 -80
- package/docs/research/provider_model_token_project_matrix_2026-02-23.md +0 -46
- package/docs/research/release_gate_checklist.md +0 -57
- package/docs/site.css +0 -505
- package/docs/sparkle.md +0 -28
- package/docs/status.md +0 -28
- package/docs/sync/implementation.md +0 -393
- package/docs/sync/readme.md +0 -317
- package/docs/sync_files_summary.md +0 -391
- package/docs/sync_quick_reference.md +0 -225
- package/docs/todo.md +0 -11
- package/docs/ui.md +0 -29
- package/docs/web-integration.md +0 -9
- package/docs/widgets.md +0 -85
- package/docs/workflows/error-handling-improvements.md +0 -434
- package/docs/workflows/folder_structure.md +0 -299
- package/docs/workflows/loading_states.md +0 -446
- package/docs/workflows/loading_states_examples.md +0 -565
- package/docs/workflows/loading_states_flow.md +0 -430
- package/docs/workflows/performance_optimizations.md +0 -457
- package/docs/workflows/phase1-cli-testing-report.md +0 -313
- package/docs/workflows/quick_reference.md +0 -228
- package/docs/workflows/readme.md +0 -59
- package/docs/workflows/recent_improvements.md +0 -275
- package/docs/workflows/sync_enablement_summary.md +0 -400
- package/docs/zai.md +0 -43
- package/runic.png +0 -0
- package/runic.svg +0 -15
- 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
|
-
}
|