@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,3480 +0,0 @@
|
|
|
1
|
-
import AppKit
|
|
2
|
-
import RunicCore
|
|
3
|
-
import SwiftUI
|
|
4
|
-
|
|
5
|
-
private struct ProviderUsageStatus {
|
|
6
|
-
let text: String
|
|
7
|
-
let style: Style
|
|
8
|
-
|
|
9
|
-
enum Style {
|
|
10
|
-
case success
|
|
11
|
-
case error
|
|
12
|
-
case neutral
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
private enum ProviderListMetrics {
|
|
17
|
-
static let contentInset: CGFloat = 16
|
|
18
|
-
static let listHeaderCornerRadius: CGFloat = RunicCornerRadius.md
|
|
19
|
-
static let listHeaderPadding: EdgeInsets = .init(
|
|
20
|
-
top: RunicSpacing.xs,
|
|
21
|
-
leading: RunicSpacing.md,
|
|
22
|
-
bottom: RunicSpacing.xs,
|
|
23
|
-
trailing: RunicSpacing.md)
|
|
24
|
-
static let listHeaderBackgroundOpacity: Double = 0.26
|
|
25
|
-
static let listHeaderBorderOpacity: Double = 0.2
|
|
26
|
-
static let rowSpacing: CGFloat = RunicSpacing.sm
|
|
27
|
-
static let reorderHandleSize: CGFloat = 12
|
|
28
|
-
static let reorderDotSize: CGFloat = 4
|
|
29
|
-
static let reorderDotSpacing: CGFloat = 4
|
|
30
|
-
static let rowInsets = EdgeInsets(
|
|
31
|
-
top: RunicSpacing.xxs,
|
|
32
|
-
leading: contentInset,
|
|
33
|
-
bottom: RunicSpacing.xxs,
|
|
34
|
-
trailing: contentInset)
|
|
35
|
-
static let sectionEdgeInset: CGFloat = RunicSpacing.md
|
|
36
|
-
static let dividerBottomInset: CGFloat = RunicSpacing.xxs
|
|
37
|
-
static let checkboxSize: CGFloat = 20
|
|
38
|
-
static let iconSize: CGFloat = 30
|
|
39
|
-
static let dividerLeadingInset: CGFloat = contentInset
|
|
40
|
-
static let dividerTrailingInset: CGFloat = contentInset
|
|
41
|
-
static let providerCardPadding = EdgeInsets(
|
|
42
|
-
top: RunicSpacing.sm,
|
|
43
|
-
leading: RunicSpacing.sm,
|
|
44
|
-
bottom: RunicSpacing.sm,
|
|
45
|
-
trailing: RunicSpacing.sm)
|
|
46
|
-
static let providerCardBackgroundOpacity: Double = 0.55
|
|
47
|
-
static let providerCardBorderOpacity: Double = 0.25
|
|
48
|
-
static let providerCardCornerRadius: CGFloat = RunicCornerRadius.md
|
|
49
|
-
static let providerInsightsCardCornerRadius: CGFloat = RunicCornerRadius.sm
|
|
50
|
-
static let providerInsightsGridItemMinWidth: CGFloat = 210
|
|
51
|
-
static let providerInsightsChipCornerRadius: CGFloat = RunicCornerRadius.sm
|
|
52
|
-
static let providerInsightsChipSpacing: CGFloat = RunicSpacing.xxs
|
|
53
|
-
static let providerInsightsChipPadding: CGFloat = RunicSpacing.xs
|
|
54
|
-
|
|
55
|
-
static let supplementalCardPadding = EdgeInsets(
|
|
56
|
-
top: RunicSpacing.sm,
|
|
57
|
-
leading: RunicSpacing.sm,
|
|
58
|
-
bottom: RunicSpacing.sm,
|
|
59
|
-
trailing: RunicSpacing.sm)
|
|
60
|
-
static let supplementalCardBackgroundOpacity: Double = 0.28
|
|
61
|
-
static let supplementalCardBorderOpacity: Double = 0.18
|
|
62
|
-
static let fieldMaxWidth: CGFloat = 420
|
|
63
|
-
static let errorCardPadding: CGFloat = RunicSpacing.sm
|
|
64
|
-
static let statusBadgePaddingH: CGFloat = RunicSpacing.xs
|
|
65
|
-
static let statusBadgePaddingV: CGFloat = RunicSpacing.xxxs
|
|
66
|
-
static let errorCardCornerRadius: CGFloat = RunicCornerRadius.sm
|
|
67
|
-
static let insightsCardPadding: CGFloat = RunicSpacing.xs
|
|
68
|
-
static let insightsLineSpacing: CGFloat = RunicSpacing.xxxs
|
|
69
|
-
static let insightsLabelWidth: CGFloat = 84
|
|
70
|
-
static let sidebarStatusLabelWidth: CGFloat = 62
|
|
71
|
-
static let sidebarCardCornerRadius: CGFloat = RunicCornerRadius.md
|
|
72
|
-
static let sidebarCardPadding: CGFloat = RunicSpacing.md
|
|
73
|
-
static let sidebarCardBackgroundOpacity: Double = 0.36
|
|
74
|
-
static let sidebarCardBorderOpacity: Double = 0.22
|
|
75
|
-
static let sidebarMicroCardCornerRadius: CGFloat = RunicCornerRadius.sm
|
|
76
|
-
static let sidebarMicroCardBackgroundOpacity: Double = 0.52
|
|
77
|
-
static let sidebarMicroCardBorderOpacity: Double = 0.2
|
|
78
|
-
static let sidebarSectionSpacing: CGFloat = RunicSpacing.md
|
|
79
|
-
static let sidebarContentGap: CGFloat = RunicSpacing.sm
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
private struct ProviderInsightLine: Identifiable, Equatable {
|
|
83
|
-
let id: String
|
|
84
|
-
let label: String
|
|
85
|
-
let value: String
|
|
86
|
-
let help: String?
|
|
87
|
-
|
|
88
|
-
init(id: String, label: String, value: String, help: String? = nil) {
|
|
89
|
-
self.id = id
|
|
90
|
-
self.label = label
|
|
91
|
-
self.value = value
|
|
92
|
-
self.help = help
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
@MainActor
|
|
97
|
-
private enum ProviderInsightsComposer {
|
|
98
|
-
static func lines(
|
|
99
|
-
for provider: UsageProvider,
|
|
100
|
-
store: UsageStore,
|
|
101
|
-
maxRows: Int? = nil) -> [ProviderInsightLine]
|
|
102
|
-
{
|
|
103
|
-
var rows: [ProviderInsightLine] = []
|
|
104
|
-
let snapshot = store.snapshot(for: provider)
|
|
105
|
-
let identity = snapshot?.identity(for: provider) ?? snapshot?.identity
|
|
106
|
-
let tokenSnapshot = store.tokenSnapshot(for: provider)
|
|
107
|
-
let attempts = store.fetchAttempts(for: provider)
|
|
108
|
-
let reliability = store.ledgerReliabilityScore(for: provider)
|
|
109
|
-
let anomaly = store.ledgerAnomalySummary(for: provider)
|
|
110
|
-
let spendForecast = store.ledgerSpendForecast(for: provider)
|
|
111
|
-
let topProjectSpendForecast = store.ledgerTopProjectSpendForecast(for: provider)
|
|
112
|
-
let topModel = store.ledgerTopModel(for: provider)
|
|
113
|
-
let topProject = store.ledgerTopProject(for: provider)
|
|
114
|
-
let modelBreakdown = store.ledgerModelBreakdown(for: provider)
|
|
115
|
-
let projectBreakdown = store.ledgerProjectBreakdown(for: provider)
|
|
116
|
-
let coverage = Self.effectiveCoverage(
|
|
117
|
-
provider: provider,
|
|
118
|
-
metadataCoverage: store.metadata(for: provider).usageCoverage,
|
|
119
|
-
topModel: topModel,
|
|
120
|
-
topProject: topProject,
|
|
121
|
-
modelBreakdown: modelBreakdown,
|
|
122
|
-
projectBreakdown: projectBreakdown,
|
|
123
|
-
snapshot: snapshot,
|
|
124
|
-
tokenSnapshot: tokenSnapshot)
|
|
125
|
-
let hasModelBreakdown = coverage.supportsModelBreakdown
|
|
126
|
-
let hasProjectAttribution = coverage.supportsProjectAttribution
|
|
127
|
-
|
|
128
|
-
if let who = self.actorValue(identity: identity) {
|
|
129
|
-
rows.append(ProviderInsightLine(id: "actor", label: "Who", value: who))
|
|
130
|
-
}
|
|
131
|
-
if let planAuth = self.planAuthValue(identity: identity) {
|
|
132
|
-
rows.append(ProviderInsightLine(id: "plan-auth", label: "Plan/Auth", value: planAuth))
|
|
133
|
-
}
|
|
134
|
-
if let fetch = self.fetchHealthValue(attempts) {
|
|
135
|
-
rows.append(ProviderInsightLine(
|
|
136
|
-
id: "fetch",
|
|
137
|
-
label: "Fetch",
|
|
138
|
-
value: fetch,
|
|
139
|
-
help: self.fetchAttemptsHelp(attempts)))
|
|
140
|
-
}
|
|
141
|
-
if store.isStale(provider: provider), let fetchError = self.fetchErrorValue(attempts) {
|
|
142
|
-
rows.append(ProviderInsightLine(
|
|
143
|
-
id: "fetch-error",
|
|
144
|
-
label: "Fetch err",
|
|
145
|
-
value: fetchError,
|
|
146
|
-
help: self.fetchAttemptsHelp(attempts)))
|
|
147
|
-
}
|
|
148
|
-
if let reliabilityValue = self.reliabilityValue(reliability) {
|
|
149
|
-
rows.append(ProviderInsightLine(
|
|
150
|
-
id: "reliability",
|
|
151
|
-
label: "Reliability",
|
|
152
|
-
value: reliabilityValue,
|
|
153
|
-
help: self.reliabilityHelpText(reliability)))
|
|
154
|
-
}
|
|
155
|
-
if let costAlertValue = self.costAnomalyValue(anomaly) {
|
|
156
|
-
rows.append(ProviderInsightLine(
|
|
157
|
-
id: "cost-alert",
|
|
158
|
-
label: "Cost alert",
|
|
159
|
-
value: costAlertValue,
|
|
160
|
-
help: self.costAnomalyHelpText(anomaly)))
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if hasModelBreakdown, let topModel, topModel.provider == provider {
|
|
164
|
-
rows.append(ProviderInsightLine(
|
|
165
|
-
id: "top-model",
|
|
166
|
-
label: "Top model",
|
|
167
|
-
value: self.topModelValue(topModel)))
|
|
168
|
-
} else if !hasModelBreakdown, let windowModels = self.windowModelsValue(snapshot) {
|
|
169
|
-
rows.append(ProviderInsightLine(
|
|
170
|
-
id: "models",
|
|
171
|
-
label: "Quota windows",
|
|
172
|
-
value: windowModels,
|
|
173
|
-
help: "Live quota windows grouped by provider response window IDs."))
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if hasProjectAttribution,
|
|
177
|
-
let topProject,
|
|
178
|
-
topProject.provider == provider
|
|
179
|
-
{
|
|
180
|
-
rows.append(ProviderInsightLine(
|
|
181
|
-
id: "top-project",
|
|
182
|
-
label: "Top project",
|
|
183
|
-
value: self.topProjectValue(topProject),
|
|
184
|
-
help: self.projectIdentityHelpText(topProject)))
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if hasModelBreakdown, let modelMix = self.modelMixValue(modelBreakdown) {
|
|
188
|
-
rows.append(ProviderInsightLine(id: "model-mix", label: "Model mix", value: modelMix))
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if hasProjectAttribution, let projectMix = self.projectMixValue(projectBreakdown) {
|
|
192
|
-
rows.append(ProviderInsightLine(
|
|
193
|
-
id: "project-mix",
|
|
194
|
-
label: "Project mix",
|
|
195
|
-
value: projectMix,
|
|
196
|
-
help: self.projectMixHelpText(projectBreakdown)))
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if let usage = self.usageValue(snapshot?.primary) {
|
|
200
|
-
rows.append(ProviderInsightLine(id: "usage", label: "Usage", value: usage))
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if let spend = self.spendValue(snapshot?.providerCost) {
|
|
204
|
-
rows.append(ProviderInsightLine(id: "spend", label: "Spend", value: spend))
|
|
205
|
-
}
|
|
206
|
-
if let forecast = self.forecastValue(spendForecast) {
|
|
207
|
-
rows.append(ProviderInsightLine(
|
|
208
|
-
id: "forecast",
|
|
209
|
-
label: "Forecast",
|
|
210
|
-
value: forecast,
|
|
211
|
-
help: self.forecastHelpText(spendForecast)))
|
|
212
|
-
}
|
|
213
|
-
if let budget = self.budgetValue(spendForecast) {
|
|
214
|
-
rows.append(ProviderInsightLine(
|
|
215
|
-
id: "budget",
|
|
216
|
-
label: "Budget",
|
|
217
|
-
value: budget,
|
|
218
|
-
help: self.budgetHelpText(spendForecast)))
|
|
219
|
-
}
|
|
220
|
-
if hasProjectAttribution, let projectBudget = self.projectBudgetValue(topProjectSpendForecast) {
|
|
221
|
-
rows.append(ProviderInsightLine(
|
|
222
|
-
id: "project-budget",
|
|
223
|
-
label: "Prj budget",
|
|
224
|
-
value: projectBudget,
|
|
225
|
-
help: self.budgetHelpText(topProjectSpendForecast)))
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if let today = self
|
|
229
|
-
.tokenWindowValue(tokens: tokenSnapshot?.sessionTokens, cost: tokenSnapshot?.sessionCostUSD)
|
|
230
|
-
{
|
|
231
|
-
rows.append(ProviderInsightLine(id: "today", label: "Today", value: today))
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if let last30 = self.tokenWindowValue(
|
|
235
|
-
tokens: tokenSnapshot?.last30DaysTokens,
|
|
236
|
-
cost: tokenSnapshot?.last30DaysCostUSD)
|
|
237
|
-
{
|
|
238
|
-
rows.append(ProviderInsightLine(id: "last30", label: "30d", value: last30))
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if let reset = self.resetValue(snapshot?.primary) {
|
|
242
|
-
rows.append(ProviderInsightLine(id: "reset", label: "Reset", value: reset))
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
guard let maxRows, maxRows > 0, rows.count > maxRows else {
|
|
246
|
-
return rows
|
|
247
|
-
}
|
|
248
|
-
return Array(rows.prefix(maxRows))
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
static func coverageSummaryLabel(for provider: UsageProvider, store: UsageStore) -> String? {
|
|
252
|
-
self.effectiveCoverage(for: provider, store: store).summaryLabel
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
static func effectiveCoverage(
|
|
256
|
-
for provider: UsageProvider,
|
|
257
|
-
store: UsageStore) -> ProviderUsageCoverage
|
|
258
|
-
{
|
|
259
|
-
self.effectiveCoverage(
|
|
260
|
-
provider: provider,
|
|
261
|
-
metadataCoverage: store.metadata(for: provider).usageCoverage,
|
|
262
|
-
topModel: store.ledgerTopModel(for: provider),
|
|
263
|
-
topProject: store.ledgerTopProject(for: provider),
|
|
264
|
-
modelBreakdown: store.ledgerModelBreakdown(for: provider),
|
|
265
|
-
projectBreakdown: store.ledgerProjectBreakdown(for: provider),
|
|
266
|
-
snapshot: store.snapshot(for: provider),
|
|
267
|
-
tokenSnapshot: store.tokenSnapshot(for: provider))
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
private static func effectiveCoverage(
|
|
271
|
-
provider: UsageProvider,
|
|
272
|
-
metadataCoverage: ProviderUsageCoverage,
|
|
273
|
-
topModel: UsageLedgerModelSummary?,
|
|
274
|
-
topProject: UsageLedgerProjectSummary?,
|
|
275
|
-
modelBreakdown: [UsageLedgerModelSummary],
|
|
276
|
-
projectBreakdown: [UsageLedgerProjectSummary],
|
|
277
|
-
snapshot: UsageSnapshot?,
|
|
278
|
-
tokenSnapshot: CostUsageTokenSnapshot?) -> ProviderUsageCoverage
|
|
279
|
-
{
|
|
280
|
-
let hasModelBreakdown =
|
|
281
|
-
metadataCoverage.supportsModelBreakdown
|
|
282
|
-
|| (topModel?.provider == provider)
|
|
283
|
-
|| !modelBreakdown.isEmpty
|
|
284
|
-
|
|
285
|
-
let hasProjectAttribution =
|
|
286
|
-
metadataCoverage.supportsProjectAttribution
|
|
287
|
-
|| (topProject?.provider == provider)
|
|
288
|
-
|| !projectBreakdown.isEmpty
|
|
289
|
-
|
|
290
|
-
let hasTokenMetrics = metadataCoverage.supportsTokenMetrics
|
|
291
|
-
|| snapshot?.providerCost != nil
|
|
292
|
-
|| tokenSnapshot?.sessionTokens != nil
|
|
293
|
-
|| tokenSnapshot?.sessionCostUSD != nil
|
|
294
|
-
|| tokenSnapshot?.last30DaysTokens != nil
|
|
295
|
-
|| tokenSnapshot?.last30DaysCostUSD != nil
|
|
296
|
-
|
|
297
|
-
return ProviderUsageCoverage(
|
|
298
|
-
supportsModelBreakdown: hasModelBreakdown,
|
|
299
|
-
supportsTokenMetrics: hasTokenMetrics,
|
|
300
|
-
supportsProjectAttribution: hasProjectAttribution)
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
private static func actorValue(identity: ProviderIdentitySnapshot?) -> String? {
|
|
304
|
-
let email = self.trimmed(identity?.accountEmail)
|
|
305
|
-
let organization = self.trimmed(identity?.accountOrganization)
|
|
306
|
-
if let email, let organization {
|
|
307
|
-
if email.caseInsensitiveCompare(organization) == .orderedSame {
|
|
308
|
-
return email
|
|
309
|
-
}
|
|
310
|
-
return "\(email) · \(organization)"
|
|
311
|
-
}
|
|
312
|
-
return email ?? organization
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
private static func planAuthValue(identity: ProviderIdentitySnapshot?) -> String? {
|
|
316
|
-
guard let raw = self.trimmed(identity?.loginMethod) else { return nil }
|
|
317
|
-
let cleaned = UsageFormatter.cleanPlanName(raw)
|
|
318
|
-
return cleaned.isEmpty ? raw : cleaned
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
private static func topModelValue(_ summary: UsageLedgerModelSummary) -> String {
|
|
322
|
-
let model = UsageFormatter.modelDisplayName(summary.model)
|
|
323
|
-
let tokens = UsageFormatter.tokenCountString(summary.totals.totalTokens)
|
|
324
|
-
var parts = [model, "\(tokens) tok", "\(summary.entryCount) req"]
|
|
325
|
-
if let context = UsageFormatter.modelContextLabel(for: summary.model) {
|
|
326
|
-
parts.append(context)
|
|
327
|
-
}
|
|
328
|
-
if let cost = summary.totals.costUSD {
|
|
329
|
-
parts.append(UsageFormatter.usdString(cost))
|
|
330
|
-
if let per1K = UsageFormatter.usdPer1KTokensString(costUSD: cost, tokenCount: summary.totals.totalTokens) {
|
|
331
|
-
parts.append("~\(per1K)")
|
|
332
|
-
}
|
|
333
|
-
if let perReq = UsageFormatter.usdPerRequestString(costUSD: cost, requestCount: summary.entryCount) {
|
|
334
|
-
parts.append("~\(perReq)")
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
return parts.joined(separator: " · ")
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
private static func topProjectValue(_ summary: UsageLedgerProjectSummary) -> String {
|
|
341
|
-
let project = self.shortProjectName(summary.displayProjectName)
|
|
342
|
-
let tokens = UsageFormatter.tokenCountString(summary.totals.totalTokens)
|
|
343
|
-
var parts = [project, "\(tokens) tok", "\(summary.entryCount) req"]
|
|
344
|
-
if let cost = summary.totals.costUSD {
|
|
345
|
-
parts.append(UsageFormatter.usdString(cost))
|
|
346
|
-
if let per1K = UsageFormatter.usdPer1KTokensString(costUSD: cost, tokenCount: summary.totals.totalTokens) {
|
|
347
|
-
parts.append("~\(per1K)")
|
|
348
|
-
}
|
|
349
|
-
if let perReq = UsageFormatter.usdPerRequestString(costUSD: cost, requestCount: summary.entryCount) {
|
|
350
|
-
parts.append("~\(perReq)")
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
return parts.joined(separator: " · ")
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
private static func modelMixValue(_ summaries: [UsageLedgerModelSummary]) -> String? {
|
|
357
|
-
guard !summaries.isEmpty else { return nil }
|
|
358
|
-
var tokensByModel: [String: Int] = [:]
|
|
359
|
-
for summary in summaries {
|
|
360
|
-
tokensByModel[summary.model, default: 0] += summary.totals.totalTokens
|
|
361
|
-
}
|
|
362
|
-
let ranked = tokensByModel.map { (model: $0.key, tokens: $0.value) }.sorted { lhs, rhs in
|
|
363
|
-
if lhs.tokens == rhs.tokens {
|
|
364
|
-
return UsageFormatter.modelDisplayName(lhs.model) < UsageFormatter.modelDisplayName(rhs.model)
|
|
365
|
-
}
|
|
366
|
-
return lhs.tokens > rhs.tokens
|
|
367
|
-
}
|
|
368
|
-
guard ranked.count > 1 else { return nil }
|
|
369
|
-
let rendered = ranked.prefix(2).map { item in
|
|
370
|
-
let name = UsageFormatter.modelDisplayName(item.model)
|
|
371
|
-
let tokens = UsageFormatter.tokenCountString(item.tokens)
|
|
372
|
-
if let context = UsageFormatter.modelContextLabel(for: item.model) {
|
|
373
|
-
return "\(name) \(context) \(tokens)"
|
|
374
|
-
}
|
|
375
|
-
return "\(name) \(tokens)"
|
|
376
|
-
}
|
|
377
|
-
var value = rendered.joined(separator: " · ")
|
|
378
|
-
if ranked.count > 2 {
|
|
379
|
-
value += " +\(ranked.count - 2) more"
|
|
380
|
-
}
|
|
381
|
-
return value
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
private static func projectMixValue(_ summaries: [UsageLedgerProjectSummary]) -> String? {
|
|
385
|
-
guard summaries.count > 1 else { return nil }
|
|
386
|
-
let ranked = summaries.sorted { lhs, rhs in
|
|
387
|
-
if lhs.totals.totalTokens == rhs.totals.totalTokens {
|
|
388
|
-
return lhs.displayProjectName < rhs.displayProjectName
|
|
389
|
-
}
|
|
390
|
-
return lhs.totals.totalTokens > rhs.totals.totalTokens
|
|
391
|
-
}
|
|
392
|
-
let rendered = ranked.prefix(2).map { summary in
|
|
393
|
-
let project = self.shortProjectName(summary.displayProjectName)
|
|
394
|
-
let tokens = UsageFormatter.tokenCountString(summary.totals.totalTokens)
|
|
395
|
-
return "\(project) \(tokens)"
|
|
396
|
-
}
|
|
397
|
-
var value = rendered.joined(separator: " · ")
|
|
398
|
-
if ranked.count > 2 {
|
|
399
|
-
value += " +\(ranked.count - 2) more"
|
|
400
|
-
}
|
|
401
|
-
return value
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
private static func shortProjectName(_ text: String) -> String {
|
|
405
|
-
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
406
|
-
let maxLength = 44
|
|
407
|
-
guard trimmed.count > maxLength else { return trimmed }
|
|
408
|
-
let prefixLength = 28
|
|
409
|
-
let suffixLength = 12
|
|
410
|
-
guard trimmed.count > (prefixLength + suffixLength + 1) else {
|
|
411
|
-
let index = trimmed.index(trimmed.startIndex, offsetBy: maxLength)
|
|
412
|
-
return "\(trimmed[..<index])…"
|
|
413
|
-
}
|
|
414
|
-
let prefixEnd = trimmed.index(trimmed.startIndex, offsetBy: prefixLength)
|
|
415
|
-
let suffixStart = trimmed.index(trimmed.endIndex, offsetBy: -suffixLength)
|
|
416
|
-
return "\(trimmed[..<prefixEnd])…\(trimmed[suffixStart...])"
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
private static func projectMixHelpText(_ summaries: [UsageLedgerProjectSummary]) -> String? {
|
|
420
|
-
guard summaries.count > 1 else { return nil }
|
|
421
|
-
let ranked = summaries.sorted { lhs, rhs in
|
|
422
|
-
if lhs.totals.totalTokens == rhs.totals.totalTokens {
|
|
423
|
-
return lhs.displayProjectName < rhs.displayProjectName
|
|
424
|
-
}
|
|
425
|
-
return lhs.totals.totalTokens > rhs.totals.totalTokens
|
|
426
|
-
}
|
|
427
|
-
let details = ranked.prefix(3).map { summary in
|
|
428
|
-
let tokens = UsageFormatter.tokenCountString(summary.totals.totalTokens)
|
|
429
|
-
let project = summary.displayProjectName
|
|
430
|
-
return "\(project): \(tokens) tok"
|
|
431
|
-
}
|
|
432
|
-
return details.isEmpty ? nil : details.joined(separator: "\n")
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
private static func projectIdentityHelpText(_ summary: UsageLedgerProjectSummary) -> String? {
|
|
436
|
-
let displayName = summary.displayProjectName
|
|
437
|
-
let source = summary.projectNameSource ?? .unknown
|
|
438
|
-
let confidence = summary.projectNameConfidence ?? .none
|
|
439
|
-
let shouldAnnotateSource = source != .projectName && source != .budgetOverride
|
|
440
|
-
let shouldAnnotateConfidence = confidence != .high
|
|
441
|
-
let isUnknown = displayName == "Unknown project"
|
|
442
|
-
|
|
443
|
-
var details: [String] = []
|
|
444
|
-
if shouldAnnotateSource {
|
|
445
|
-
details.append("source: \(self.projectSourceLabel(source))")
|
|
446
|
-
}
|
|
447
|
-
if shouldAnnotateConfidence {
|
|
448
|
-
details.append("confidence: \(self.projectConfidenceLabel(confidence))")
|
|
449
|
-
}
|
|
450
|
-
if isUnknown, let fingerprint = self.projectIDFingerprint(summary.projectID) {
|
|
451
|
-
details.append("id: \(fingerprint)")
|
|
452
|
-
}
|
|
453
|
-
if let provenance = self.trimmed(summary.projectNameProvenance) {
|
|
454
|
-
details.append("via: \(provenance)")
|
|
455
|
-
}
|
|
456
|
-
guard !details.isEmpty else { return nil }
|
|
457
|
-
return ([displayName] + details).joined(separator: "\n")
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
private static func projectSourceLabel(_ source: UsageLedgerProjectNameSource) -> String {
|
|
461
|
-
switch source {
|
|
462
|
-
case .projectName: "project name"
|
|
463
|
-
case .projectID: "project id"
|
|
464
|
-
case .inferredFromPath: "path-derived"
|
|
465
|
-
case .inferredFromName: "name-derived"
|
|
466
|
-
case .budgetOverride: "budget override"
|
|
467
|
-
case .unknown: "unknown"
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
private static func projectConfidenceLabel(_ confidence: UsageLedgerProjectNameConfidence) -> String {
|
|
472
|
-
switch confidence {
|
|
473
|
-
case .high: "high"
|
|
474
|
-
case .medium: "medium"
|
|
475
|
-
case .low: "low"
|
|
476
|
-
case .none: "none"
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
private static func projectIDFingerprint(_ projectID: String?) -> String? {
|
|
481
|
-
guard let projectID = projectID?.trimmingCharacters(in: .whitespacesAndNewlines), !projectID.isEmpty else {
|
|
482
|
-
return nil
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
var hash: UInt64 = 0xCBF2_9CE4_8422_2325
|
|
486
|
-
for byte in projectID.lowercased().utf8 {
|
|
487
|
-
hash ^= UInt64(byte)
|
|
488
|
-
hash = hash &* 0x100_0000_01B3
|
|
489
|
-
}
|
|
490
|
-
return String(format: "%08llx", hash)
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
private static func trimmed(_ value: String?) -> String? {
|
|
494
|
-
guard let value else { return nil }
|
|
495
|
-
let cleaned = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
496
|
-
return cleaned.isEmpty ? nil : cleaned
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
private static func usageValue(_ window: RateWindow?) -> String? {
|
|
500
|
-
guard let window else { return nil }
|
|
501
|
-
var parts = ["\(Int(window.usedPercent.rounded()))% used"]
|
|
502
|
-
if let minutes = window.windowMinutes, minutes > 0 {
|
|
503
|
-
parts.append("\(minutes)m window")
|
|
504
|
-
}
|
|
505
|
-
return parts.joined(separator: " · ")
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
private static func spendValue(_ providerCost: ProviderCostSnapshot?) -> String? {
|
|
509
|
-
guard let providerCost else { return nil }
|
|
510
|
-
let used = UsageFormatter.currencyString(providerCost.used, currencyCode: providerCost.currencyCode)
|
|
511
|
-
var value: String
|
|
512
|
-
if providerCost.limit > 0 {
|
|
513
|
-
let limitText = UsageFormatter.currencyString(providerCost.limit, currencyCode: providerCost.currencyCode)
|
|
514
|
-
value = "\(used) / \(limitText)"
|
|
515
|
-
} else {
|
|
516
|
-
value = used
|
|
517
|
-
}
|
|
518
|
-
if let period = self.trimmed(providerCost.period) {
|
|
519
|
-
value += " · \(period)"
|
|
520
|
-
}
|
|
521
|
-
return value
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
private static func forecastValue(_ forecast: UsageLedgerSpendForecast?) -> String? {
|
|
525
|
-
guard let forecast else { return nil }
|
|
526
|
-
var parts = [UsageFormatter.usdString(forecast.projected30DayCostUSD), "/ 30d"]
|
|
527
|
-
if forecast.averageDailyCostUSD.isFinite, forecast.averageDailyCostUSD >= 0 {
|
|
528
|
-
parts.append("· \(UsageFormatter.usdString(forecast.averageDailyCostUSD))/day")
|
|
529
|
-
}
|
|
530
|
-
return parts.joined(separator: " ")
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
private static func budgetValue(_ forecast: UsageLedgerSpendForecast?) -> String? {
|
|
534
|
-
guard let forecast, let limit = forecast.budgetLimitUSD, limit > 0 else { return nil }
|
|
535
|
-
let limitText = UsageFormatter.usdString(limit)
|
|
536
|
-
if let eta = forecast.budgetETAInDays {
|
|
537
|
-
return "\(limitText) · \(self.budgetBreachETAText(days: eta))"
|
|
538
|
-
}
|
|
539
|
-
if forecast.budgetWillBreach {
|
|
540
|
-
return "\(limitText) · Breach risk"
|
|
541
|
-
}
|
|
542
|
-
return "\(limitText) · On track"
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
private static func projectBudgetValue(_ forecast: UsageLedgerSpendForecast?) -> String? {
|
|
546
|
-
guard let forecast, let limit = forecast.budgetLimitUSD, limit > 0 else { return nil }
|
|
547
|
-
let name = self
|
|
548
|
-
.shortProjectName(forecast.projectName?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Top project")
|
|
549
|
-
if let eta = forecast.budgetETAInDays {
|
|
550
|
-
return "\(name) · \(self.budgetBreachETAText(days: eta))"
|
|
551
|
-
}
|
|
552
|
-
if forecast.budgetWillBreach {
|
|
553
|
-
return "\(name) · Breach risk"
|
|
554
|
-
}
|
|
555
|
-
return "\(name) · On track"
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
private static func forecastHelpText(_ forecast: UsageLedgerSpendForecast?) -> String? {
|
|
559
|
-
guard let forecast else { return nil }
|
|
560
|
-
var lines: [String] = []
|
|
561
|
-
let observedLabel = forecast.observedDays == 1 ? "day" : "days"
|
|
562
|
-
lines.append("Observed: \(forecast.observedDays) \(observedLabel)")
|
|
563
|
-
lines.append("Projected 30d: \(UsageFormatter.usdString(forecast.projected30DayCostUSD))")
|
|
564
|
-
if let p50 = forecast.projectedCostP50USD {
|
|
565
|
-
lines.append("p50: \(UsageFormatter.usdString(p50))")
|
|
566
|
-
}
|
|
567
|
-
if let p80 = forecast.projectedCostP80USD {
|
|
568
|
-
lines.append("p80: \(UsageFormatter.usdString(p80))")
|
|
569
|
-
}
|
|
570
|
-
if let p95 = forecast.projectedCostP95USD {
|
|
571
|
-
lines.append("p95: \(UsageFormatter.usdString(p95))")
|
|
572
|
-
}
|
|
573
|
-
return lines.joined(separator: "\n")
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
private static func budgetHelpText(_ forecast: UsageLedgerSpendForecast?) -> String? {
|
|
577
|
-
guard let forecast, let limit = forecast.budgetLimitUSD, limit > 0 else { return nil }
|
|
578
|
-
var lines = [
|
|
579
|
-
"Budget limit: \(UsageFormatter.usdString(limit))",
|
|
580
|
-
"Projected 30d: \(UsageFormatter.usdString(forecast.projected30DayCostUSD))",
|
|
581
|
-
]
|
|
582
|
-
if let eta = forecast.budgetETAInDays {
|
|
583
|
-
lines.append(self.budgetBreachETAText(days: eta))
|
|
584
|
-
} else if forecast.budgetWillBreach {
|
|
585
|
-
lines.append("Breach risk at current pace")
|
|
586
|
-
} else {
|
|
587
|
-
lines.append("No breach at current pace")
|
|
588
|
-
}
|
|
589
|
-
return lines.joined(separator: "\n")
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
private static func budgetBreachETAText(days: Double) -> String {
|
|
593
|
-
guard days.isFinite else { return "Breach ETA unavailable" }
|
|
594
|
-
if days <= 0 { return "Breach now" }
|
|
595
|
-
let now = Date()
|
|
596
|
-
let etaDate = now.addingTimeInterval(days * 24 * 60 * 60)
|
|
597
|
-
let countdown = UsageFormatter.resetCountdownDescription(from: etaDate, now: now)
|
|
598
|
-
if countdown == "now" { return "Breach now" }
|
|
599
|
-
return "Breach \(countdown)"
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
private static func resetValue(_ window: RateWindow?) -> String? {
|
|
603
|
-
guard let window else { return nil }
|
|
604
|
-
if let resetsAt = window.resetsAt {
|
|
605
|
-
return UsageFormatter.resetCountdownDescription(from: resetsAt)
|
|
606
|
-
}
|
|
607
|
-
return self.trimmed(window.resetDescription)
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
private static func tokenWindowValue(tokens: Int?, cost: Double?) -> String? {
|
|
611
|
-
var parts: [String] = []
|
|
612
|
-
if let cost, cost.isFinite, cost >= 0 {
|
|
613
|
-
parts.append(UsageFormatter.usdString(cost))
|
|
614
|
-
}
|
|
615
|
-
if let tokens, tokens >= 0 {
|
|
616
|
-
parts.append("\(UsageFormatter.tokenCountString(tokens)) tok")
|
|
617
|
-
}
|
|
618
|
-
return parts.isEmpty ? nil : parts.joined(separator: " · ")
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
private static func reliabilityValue(_ reliability: UsageLedgerReliabilityScore?) -> String? {
|
|
622
|
-
guard let reliability else { return nil }
|
|
623
|
-
return "\(reliability.grade) · \(reliability.score)/100"
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
private static func reliabilityHelpText(_ reliability: UsageLedgerReliabilityScore?) -> String? {
|
|
627
|
-
guard let reliability else { return nil }
|
|
628
|
-
var lines = [reliability.summary]
|
|
629
|
-
if let primary = self.trimmed(reliability.primarySignal), !primary.isEmpty {
|
|
630
|
-
lines.append(primary)
|
|
631
|
-
}
|
|
632
|
-
for signal in reliability.signals {
|
|
633
|
-
let trimmed = self.trimmed(signal) ?? ""
|
|
634
|
-
guard !trimmed.isEmpty else { continue }
|
|
635
|
-
if lines.contains(trimmed) { continue }
|
|
636
|
-
lines.append(trimmed)
|
|
637
|
-
if lines.count >= 5 { break }
|
|
638
|
-
}
|
|
639
|
-
return lines.joined(separator: "\n")
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
private static func costAnomalyValue(_ anomaly: UsageLedgerAnomalySummary?) -> String? {
|
|
643
|
-
guard let spend = anomaly?.spendAnomaly else { return nil }
|
|
644
|
-
let percent = Int((spend.percentIncrease * 100).rounded())
|
|
645
|
-
return "\(spend.severity.label) spend +\(percent)%"
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
private static func costAnomalyHelpText(_ anomaly: UsageLedgerAnomalySummary?) -> String? {
|
|
649
|
-
guard let anomaly, let spend = anomaly.spendAnomaly else { return nil }
|
|
650
|
-
let percent = Int((spend.percentIncrease * 100).rounded())
|
|
651
|
-
var lines = [
|
|
652
|
-
"Spend today: \(UsageFormatter.usdString(spend.todayValue))",
|
|
653
|
-
"Baseline (\(anomaly.baselineDays)d avg): \(UsageFormatter.usdString(spend.baselineAverage))",
|
|
654
|
-
"Increase: +\(percent)%",
|
|
655
|
-
"Severity: \(spend.severity.label)",
|
|
656
|
-
]
|
|
657
|
-
if let explanation = anomaly.explanation {
|
|
658
|
-
lines.append(contentsOf: explanation.details.prefix(2))
|
|
659
|
-
}
|
|
660
|
-
return lines.joined(separator: "\n")
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
private static func fetchHealthValue(_ attempts: [ProviderFetchAttempt]) -> String? {
|
|
664
|
-
guard !attempts.isEmpty else { return nil }
|
|
665
|
-
let rendered = attempts.prefix(3).map { attempt in
|
|
666
|
-
let status = if !attempt.wasAvailable {
|
|
667
|
-
"unavailable"
|
|
668
|
-
} else if self.trimmed(attempt.errorDescription) != nil {
|
|
669
|
-
"failed"
|
|
670
|
-
} else {
|
|
671
|
-
"ok"
|
|
672
|
-
}
|
|
673
|
-
return "\(self.fetchStrategyLabel(attempt)) \(status)"
|
|
674
|
-
}
|
|
675
|
-
var value = rendered.joined(separator: " · ")
|
|
676
|
-
if attempts.count > 3 {
|
|
677
|
-
value += " +\(attempts.count - 3) more"
|
|
678
|
-
}
|
|
679
|
-
return value
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
private static func fetchErrorValue(_ attempts: [ProviderFetchAttempt]) -> String? {
|
|
683
|
-
guard let latestError = attempts.reversed().compactMap({ self.trimmed($0.errorDescription) }).first else {
|
|
684
|
-
return nil
|
|
685
|
-
}
|
|
686
|
-
return self.truncated(latestError, maxLength: 88)
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
private static func fetchAttemptsHelp(_ attempts: [ProviderFetchAttempt]) -> String? {
|
|
690
|
-
guard !attempts.isEmpty else { return nil }
|
|
691
|
-
return attempts.map { attempt in
|
|
692
|
-
let status: String
|
|
693
|
-
if !attempt.wasAvailable {
|
|
694
|
-
status = "unavailable"
|
|
695
|
-
} else if let error = self.trimmed(attempt.errorDescription) {
|
|
696
|
-
return "\(attempt.strategyID) (\(self.fetchKindLabel(attempt.kind))) failed: \(error)"
|
|
697
|
-
} else {
|
|
698
|
-
status = "ok"
|
|
699
|
-
}
|
|
700
|
-
return "\(attempt.strategyID) (\(self.fetchKindLabel(attempt.kind))) \(status)"
|
|
701
|
-
}.joined(separator: "\n")
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
private static func fetchStrategyLabel(_ attempt: ProviderFetchAttempt) -> String {
|
|
705
|
-
let raw = attempt.strategyID
|
|
706
|
-
.replacingOccurrences(of: "_", with: " ")
|
|
707
|
-
.replacingOccurrences(of: "-", with: " ")
|
|
708
|
-
let cleaned = UsageFormatter.cleanPlanName(raw).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
709
|
-
if !cleaned.isEmpty {
|
|
710
|
-
return cleaned
|
|
711
|
-
}
|
|
712
|
-
return self.fetchKindLabel(attempt.kind)
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
private static func fetchKindLabel(_ kind: ProviderFetchKind) -> String {
|
|
716
|
-
switch kind {
|
|
717
|
-
case .cli: "cli"
|
|
718
|
-
case .web: "web"
|
|
719
|
-
case .oauth: "oauth"
|
|
720
|
-
case .apiToken: "api"
|
|
721
|
-
case .api: "api"
|
|
722
|
-
case .localProbe: "local"
|
|
723
|
-
case .webDashboard: "web"
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
private static func truncated(_ text: String, maxLength: Int) -> String {
|
|
728
|
-
guard text.count > maxLength else { return text }
|
|
729
|
-
let index = text.index(text.startIndex, offsetBy: maxLength)
|
|
730
|
-
return "\(text[..<index])…"
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
private static func windowModelsValue(_ snapshot: UsageSnapshot?) -> String? {
|
|
734
|
-
guard let snapshot else { return nil }
|
|
735
|
-
let windows = [snapshot.primary, snapshot.secondary, snapshot.tertiary].compactMap(\.self)
|
|
736
|
-
guard !windows.isEmpty else { return nil }
|
|
737
|
-
|
|
738
|
-
var seen: Set<String> = []
|
|
739
|
-
var items: [String] = []
|
|
740
|
-
for window in windows {
|
|
741
|
-
guard let rawLabel = self.trimmed(window.label) else { continue }
|
|
742
|
-
let normalized = rawLabel.lowercased()
|
|
743
|
-
guard !seen.contains(normalized) else { continue }
|
|
744
|
-
seen.insert(normalized)
|
|
745
|
-
|
|
746
|
-
let label = UsageFormatter.modelDisplayName(rawLabel)
|
|
747
|
-
let used = Int(window.usedPercent.rounded())
|
|
748
|
-
items.append("\(label) \(used)%")
|
|
749
|
-
}
|
|
750
|
-
guard !items.isEmpty else { return nil }
|
|
751
|
-
if items.count > 3 {
|
|
752
|
-
let visible = items.prefix(3).joined(separator: " · ")
|
|
753
|
-
return "\(visible) +\(items.count - 3) more"
|
|
754
|
-
}
|
|
755
|
-
return items.joined(separator: " · ")
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
@MainActor
|
|
760
|
-
struct ProvidersPane: View {
|
|
761
|
-
@Bindable var settings: SettingsStore
|
|
762
|
-
@Bindable var store: UsageStore
|
|
763
|
-
@State private var expandedErrors: Set<UsageProvider> = []
|
|
764
|
-
@State private var settingsStatusTextByID: [String: String] = [:]
|
|
765
|
-
@State private var settingsLastAppActiveRunAtByID: [String: Date] = [:]
|
|
766
|
-
@State private var activeConfirmation: ProviderSettingsConfirmationState?
|
|
767
|
-
@State private var sidebarSelection: UsageProvider?
|
|
768
|
-
|
|
769
|
-
private var providers: [UsageProvider] {
|
|
770
|
-
self.settings.orderedProviders()
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
var body: some View {
|
|
774
|
-
Group {
|
|
775
|
-
if self.settings.providersPaneSidebar {
|
|
776
|
-
self.sidebarLayout
|
|
777
|
-
} else {
|
|
778
|
-
self.listLayout
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
|
782
|
-
self.runSettingsDidBecomeActiveHooks()
|
|
783
|
-
}
|
|
784
|
-
.alert(
|
|
785
|
-
self.activeConfirmation?.title ?? "",
|
|
786
|
-
isPresented: Binding(
|
|
787
|
-
get: { self.activeConfirmation != nil },
|
|
788
|
-
set: { isPresented in
|
|
789
|
-
if !isPresented { self.activeConfirmation = nil }
|
|
790
|
-
}),
|
|
791
|
-
actions: {
|
|
792
|
-
if let active = self.activeConfirmation {
|
|
793
|
-
Button(active.confirmTitle) {
|
|
794
|
-
active.onConfirm()
|
|
795
|
-
self.activeConfirmation = nil
|
|
796
|
-
}
|
|
797
|
-
Button("Cancel", role: .cancel) { self.activeConfirmation = nil }
|
|
798
|
-
}
|
|
799
|
-
},
|
|
800
|
-
message: {
|
|
801
|
-
if let active = self.activeConfirmation {
|
|
802
|
-
Text(active.message)
|
|
803
|
-
}
|
|
804
|
-
})
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
// MARK: - List layout (default)
|
|
808
|
-
|
|
809
|
-
private var listLayout: some View {
|
|
810
|
-
PreferencesListPane(horizontalPadding: 0, verticalPadding: 0) {
|
|
811
|
-
VStack(spacing: ProviderListMetrics.sidebarSectionSpacing) {
|
|
812
|
-
self.listLayoutHeader
|
|
813
|
-
|
|
814
|
-
ProviderListView(
|
|
815
|
-
providers: self.providers,
|
|
816
|
-
store: self.store,
|
|
817
|
-
isEnabled: { provider in self.binding(for: provider) },
|
|
818
|
-
subtitle: { provider in self.providerSubtitle(provider) },
|
|
819
|
-
usageStatus: { provider in self.providerUsageStatus(provider) },
|
|
820
|
-
sourceLabel: { provider in self.providerSourceLabel(provider) },
|
|
821
|
-
statusLabel: { provider in self.providerStatusLabel(provider) },
|
|
822
|
-
settingsToggles: { provider in self.extraSettingsToggles(for: provider) },
|
|
823
|
-
settingsFields: { provider in self.extraSettingsFields(for: provider) },
|
|
824
|
-
errorDisplay: { provider in self.providerErrorDisplay(provider) },
|
|
825
|
-
isErrorExpanded: { provider in self.expandedBinding(for: provider) },
|
|
826
|
-
onCopyError: { text in self.copyToPasteboard(text) },
|
|
827
|
-
moveProviders: { fromOffsets, toOffset in
|
|
828
|
-
self.settings.moveProvider(fromOffsets: fromOffsets, toOffset: toOffset)
|
|
829
|
-
})
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
private var listLayoutHeader: some View {
|
|
835
|
-
HStack(alignment: .top, spacing: RunicSpacing.sm) {
|
|
836
|
-
Image(systemName: "square.grid.2x2")
|
|
837
|
-
.font(.system(size: 15, weight: .medium))
|
|
838
|
-
.foregroundStyle(.secondary)
|
|
839
|
-
|
|
840
|
-
VStack(alignment: .leading, spacing: RunicSpacing.xxs) {
|
|
841
|
-
Text("Built-in providers")
|
|
842
|
-
.font(RunicFont.subheadline.weight(.semibold))
|
|
843
|
-
.foregroundStyle(.primary)
|
|
844
|
-
Text("Switch to sidebar layout for per-day history cards and model or project drill-down details.")
|
|
845
|
-
.font(RunicFont.caption)
|
|
846
|
-
.foregroundStyle(.secondary)
|
|
847
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
Spacer(minLength: 0)
|
|
851
|
-
}
|
|
852
|
-
.padding(ProviderListMetrics.listHeaderPadding)
|
|
853
|
-
.background(
|
|
854
|
-
RoundedRectangle(cornerRadius: ProviderListMetrics.listHeaderCornerRadius, style: .continuous)
|
|
855
|
-
.fill(Color(nsColor: .controlBackgroundColor).opacity(ProviderListMetrics.listHeaderBackgroundOpacity)))
|
|
856
|
-
.overlay(
|
|
857
|
-
RoundedRectangle(cornerRadius: ProviderListMetrics.listHeaderCornerRadius, style: .continuous)
|
|
858
|
-
.strokeBorder(
|
|
859
|
-
Color(nsColor: .separatorColor).opacity(ProviderListMetrics.listHeaderBorderOpacity),
|
|
860
|
-
lineWidth: 1))
|
|
861
|
-
.padding(.horizontal, ProviderListMetrics.contentInset)
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// MARK: - Sidebar layout
|
|
865
|
-
|
|
866
|
-
private var sidebarLayout: some View {
|
|
867
|
-
NavigationSplitView {
|
|
868
|
-
List(selection: self.$sidebarSelection) {
|
|
869
|
-
ForEach(self.providers, id: \.self) { provider in
|
|
870
|
-
ProviderSidebarRow(
|
|
871
|
-
provider: provider,
|
|
872
|
-
store: self.store,
|
|
873
|
-
isEnabled: self.binding(for: provider).wrappedValue,
|
|
874
|
-
isSelected: self.sidebarSelection == provider)
|
|
875
|
-
.tag(provider)
|
|
876
|
-
}
|
|
877
|
-
.onMove { fromOffsets, toOffset in
|
|
878
|
-
self.settings.moveProvider(fromOffsets: fromOffsets, toOffset: toOffset)
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
.listStyle(.sidebar)
|
|
882
|
-
.frame(minWidth: 160, idealWidth: 180)
|
|
883
|
-
.scrollContentBackground(.hidden)
|
|
884
|
-
.background(Color(nsColor: .controlBackgroundColor).opacity(0.15))
|
|
885
|
-
.onAppear {
|
|
886
|
-
self.normalizeSidebarSelection()
|
|
887
|
-
}
|
|
888
|
-
.onChange(of: self.providers) { _, _ in
|
|
889
|
-
self.normalizeSidebarSelection()
|
|
890
|
-
}
|
|
891
|
-
} detail: {
|
|
892
|
-
if let selected = self.sidebarSelection {
|
|
893
|
-
ProviderSidebarDetailView(
|
|
894
|
-
provider: selected,
|
|
895
|
-
store: self.store,
|
|
896
|
-
isEnabled: self.binding(for: selected),
|
|
897
|
-
subtitle: self.providerSubtitle(selected),
|
|
898
|
-
usageStatus: self.providerUsageStatus(selected),
|
|
899
|
-
sourceLabel: self.providerSourceLabel(selected),
|
|
900
|
-
statusLabel: self.providerStatusLabel(selected),
|
|
901
|
-
settingsToggles: self.extraSettingsToggles(for: selected),
|
|
902
|
-
settingsFields: self.extraSettingsFields(for: selected),
|
|
903
|
-
errorDisplay: self.providerErrorDisplay(selected),
|
|
904
|
-
isErrorExpanded: self.expandedBinding(for: selected),
|
|
905
|
-
onCopyError: { text in self.copyToPasteboard(text) })
|
|
906
|
-
} else {
|
|
907
|
-
Text("Select a provider")
|
|
908
|
-
.font(RunicFont.title3)
|
|
909
|
-
.foregroundStyle(.secondary)
|
|
910
|
-
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
private func normalizeSidebarSelection() {
|
|
916
|
-
guard !self.providers.isEmpty else {
|
|
917
|
-
self.sidebarSelection = nil
|
|
918
|
-
return
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
if let selected = self.sidebarSelection, self.providers.contains(selected) {
|
|
922
|
-
return
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
self.sidebarSelection = self.providers.first
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
private func binding(for provider: UsageProvider) -> Binding<Bool> {
|
|
929
|
-
let meta = self.store.metadata(for: provider)
|
|
930
|
-
return Binding(
|
|
931
|
-
get: { self.settings.isProviderEnabled(provider: provider, metadata: meta) },
|
|
932
|
-
set: { self.settings.setProviderEnabled(provider: provider, metadata: meta, enabled: $0) })
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
private func providerSubtitle(_ provider: UsageProvider) -> String {
|
|
936
|
-
let meta = self.store.metadata(for: provider)
|
|
937
|
-
let cliName = meta.cliName
|
|
938
|
-
let coverageSuffix = ProviderInsightsComposer.coverageSummaryLabel(
|
|
939
|
-
for: provider,
|
|
940
|
-
store: self.store).map { " • \($0)" } ?? ""
|
|
941
|
-
let version = self.store.version(for: provider)
|
|
942
|
-
var versionText = version ?? "not detected"
|
|
943
|
-
if provider == .claude, let parenRange = versionText.range(of: "(") {
|
|
944
|
-
versionText = versionText[..<parenRange.lowerBound].trimmingCharacters(in: .whitespaces)
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
if cliName == "codex" {
|
|
948
|
-
return "\(versionText)\(coverageSuffix)"
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
// Cursor is web-based, no CLI version to detect
|
|
952
|
-
if provider == .cursor || provider == .minimax {
|
|
953
|
-
return "web\(coverageSuffix)"
|
|
954
|
-
}
|
|
955
|
-
let apiBackedProviders: Set<UsageProvider> = [
|
|
956
|
-
.zai,
|
|
957
|
-
.openrouter,
|
|
958
|
-
.groq,
|
|
959
|
-
.deepseek,
|
|
960
|
-
.fireworks,
|
|
961
|
-
.mistral,
|
|
962
|
-
.perplexity,
|
|
963
|
-
.kimi,
|
|
964
|
-
.auggie,
|
|
965
|
-
.together,
|
|
966
|
-
.cohere,
|
|
967
|
-
.xai,
|
|
968
|
-
.cerebras,
|
|
969
|
-
.sambanova,
|
|
970
|
-
.azure,
|
|
971
|
-
.bedrock,
|
|
972
|
-
]
|
|
973
|
-
if apiBackedProviders.contains(provider) {
|
|
974
|
-
return "api\(coverageSuffix)"
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
var detail = "\(cliName) \(versionText)"
|
|
978
|
-
if provider == .antigravity {
|
|
979
|
-
detail += " • experimental"
|
|
980
|
-
}
|
|
981
|
-
return "\(detail)\(coverageSuffix)"
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
private func providerUsageStatus(_ provider: UsageProvider) -> ProviderUsageStatus {
|
|
985
|
-
if let snapshot = self.store.snapshot(for: provider) {
|
|
986
|
-
let relative = snapshot.updatedAt.relativeDescription()
|
|
987
|
-
return ProviderUsageStatus(text: "usage fetched \(relative)", style: .success)
|
|
988
|
-
} else if self.store.isStale(provider: provider) {
|
|
989
|
-
return ProviderUsageStatus(text: "last fetch failed", style: .error)
|
|
990
|
-
} else {
|
|
991
|
-
return ProviderUsageStatus(text: "usage not fetched yet", style: .neutral)
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
private func providerSourceLabel(_ provider: UsageProvider) -> String {
|
|
996
|
-
self.store.sourceLabel(for: provider)
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
private func providerStatusLabel(_ provider: UsageProvider) -> String {
|
|
1000
|
-
if let snapshot = self.store.snapshot(for: provider) {
|
|
1001
|
-
return snapshot.updatedAt.formatted(date: .abbreviated, time: .shortened)
|
|
1002
|
-
}
|
|
1003
|
-
if self.store.isStale(provider: provider) {
|
|
1004
|
-
return "failed"
|
|
1005
|
-
}
|
|
1006
|
-
return "not yet"
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
private func providerErrorDisplay(_ provider: UsageProvider) -> ProviderErrorDisplay? {
|
|
1010
|
-
guard self.store.isStale(provider: provider), let raw = self.store.error(for: provider) else { return nil }
|
|
1011
|
-
return ProviderErrorDisplay(
|
|
1012
|
-
preview: self.truncated(raw, prefix: ""),
|
|
1013
|
-
full: raw)
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
private func extraSettingsToggles(for provider: UsageProvider) -> [ProviderSettingsToggleDescriptor] {
|
|
1017
|
-
guard let impl = ProviderCatalog.implementation(for: provider) else { return [] }
|
|
1018
|
-
let context = self.makeSettingsContext(provider: provider)
|
|
1019
|
-
return impl.settingsToggles(context: context)
|
|
1020
|
-
.filter { $0.isVisible?() ?? true }
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
private func extraSettingsFields(for provider: UsageProvider) -> [ProviderSettingsFieldDescriptor] {
|
|
1024
|
-
guard let impl = ProviderCatalog.implementation(for: provider) else { return [] }
|
|
1025
|
-
let context = self.makeSettingsContext(provider: provider)
|
|
1026
|
-
return impl.settingsFields(context: context)
|
|
1027
|
-
.filter { $0.isVisible?() ?? true }
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
private func makeSettingsContext(provider: UsageProvider) -> ProviderSettingsContext {
|
|
1031
|
-
ProviderSettingsContext(
|
|
1032
|
-
provider: provider,
|
|
1033
|
-
settings: self.settings,
|
|
1034
|
-
store: self.store,
|
|
1035
|
-
boolBinding: { keyPath in
|
|
1036
|
-
Binding(
|
|
1037
|
-
get: { self.settings[keyPath: keyPath] },
|
|
1038
|
-
set: { self.settings[keyPath: keyPath] = $0 })
|
|
1039
|
-
},
|
|
1040
|
-
stringBinding: { keyPath in
|
|
1041
|
-
Binding(
|
|
1042
|
-
get: { self.settings[keyPath: keyPath] },
|
|
1043
|
-
set: { self.settings[keyPath: keyPath] = $0 })
|
|
1044
|
-
},
|
|
1045
|
-
statusText: { id in
|
|
1046
|
-
self.settingsStatusTextByID[id]
|
|
1047
|
-
},
|
|
1048
|
-
setStatusText: { id, text in
|
|
1049
|
-
if let text {
|
|
1050
|
-
self.settingsStatusTextByID[id] = text
|
|
1051
|
-
} else {
|
|
1052
|
-
self.settingsStatusTextByID.removeValue(forKey: id)
|
|
1053
|
-
}
|
|
1054
|
-
},
|
|
1055
|
-
lastAppActiveRunAt: { id in
|
|
1056
|
-
self.settingsLastAppActiveRunAtByID[id]
|
|
1057
|
-
},
|
|
1058
|
-
setLastAppActiveRunAt: { id, date in
|
|
1059
|
-
if let date {
|
|
1060
|
-
self.settingsLastAppActiveRunAtByID[id] = date
|
|
1061
|
-
} else {
|
|
1062
|
-
self.settingsLastAppActiveRunAtByID.removeValue(forKey: id)
|
|
1063
|
-
}
|
|
1064
|
-
},
|
|
1065
|
-
requestConfirmation: { confirmation in
|
|
1066
|
-
self.activeConfirmation = ProviderSettingsConfirmationState(confirmation: confirmation)
|
|
1067
|
-
})
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
private func runSettingsDidBecomeActiveHooks() {
|
|
1071
|
-
for provider in UsageProvider.allCases {
|
|
1072
|
-
for toggle in self.extraSettingsToggles(for: provider) {
|
|
1073
|
-
guard let hook = toggle.onAppDidBecomeActive else { continue }
|
|
1074
|
-
Task { @MainActor in
|
|
1075
|
-
await hook()
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
private func truncated(_ text: String, prefix: String, maxLength: Int = 160) -> String {
|
|
1082
|
-
var message = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1083
|
-
if message.count > maxLength {
|
|
1084
|
-
let idx = message.index(message.startIndex, offsetBy: maxLength)
|
|
1085
|
-
message = "\(message[..<idx])…"
|
|
1086
|
-
}
|
|
1087
|
-
return prefix + message
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
private func expandedBinding(for provider: UsageProvider) -> Binding<Bool> {
|
|
1091
|
-
Binding(
|
|
1092
|
-
get: { self.expandedErrors.contains(provider) },
|
|
1093
|
-
set: { expanded in
|
|
1094
|
-
if expanded {
|
|
1095
|
-
self.expandedErrors.insert(provider)
|
|
1096
|
-
} else {
|
|
1097
|
-
self.expandedErrors.remove(provider)
|
|
1098
|
-
}
|
|
1099
|
-
})
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
private func copyToPasteboard(_ text: String) {
|
|
1103
|
-
let pb = NSPasteboard.general
|
|
1104
|
-
pb.clearContents()
|
|
1105
|
-
pb.setString(text, forType: .string)
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
@MainActor
|
|
1110
|
-
private struct ProviderListView: View {
|
|
1111
|
-
let providers: [UsageProvider]
|
|
1112
|
-
@Bindable var store: UsageStore
|
|
1113
|
-
let isEnabled: (UsageProvider) -> Binding<Bool>
|
|
1114
|
-
let subtitle: (UsageProvider) -> String
|
|
1115
|
-
let usageStatus: (UsageProvider) -> ProviderUsageStatus
|
|
1116
|
-
let sourceLabel: (UsageProvider) -> String
|
|
1117
|
-
let statusLabel: (UsageProvider) -> String
|
|
1118
|
-
let settingsToggles: (UsageProvider) -> [ProviderSettingsToggleDescriptor]
|
|
1119
|
-
let settingsFields: (UsageProvider) -> [ProviderSettingsFieldDescriptor]
|
|
1120
|
-
let errorDisplay: (UsageProvider) -> ProviderErrorDisplay?
|
|
1121
|
-
let isErrorExpanded: (UsageProvider) -> Binding<Bool>
|
|
1122
|
-
let onCopyError: (String) -> Void
|
|
1123
|
-
let moveProviders: (IndexSet, Int) -> Void
|
|
1124
|
-
|
|
1125
|
-
var body: some View {
|
|
1126
|
-
List {
|
|
1127
|
-
ForEach(self.providers, id: \.self) { provider in
|
|
1128
|
-
let fields = self.settingsFields(provider)
|
|
1129
|
-
let toggles = self.settingsToggles(provider)
|
|
1130
|
-
let isEnabled = self.isEnabled(provider).wrappedValue
|
|
1131
|
-
let isFirstProvider = provider == self.providers.first
|
|
1132
|
-
let isLastProvider = provider == self.providers.last
|
|
1133
|
-
let shouldShowDivider = provider != self.providers.last
|
|
1134
|
-
let showDividerOnProviderRow = shouldShowDivider &&
|
|
1135
|
-
(!isEnabled || (fields.isEmpty && toggles.isEmpty))
|
|
1136
|
-
let providerAddsBottomPadding = isLastProvider && (!isEnabled || (fields.isEmpty && toggles.isEmpty))
|
|
1137
|
-
|
|
1138
|
-
ProviderListProviderRowView(
|
|
1139
|
-
provider: provider,
|
|
1140
|
-
store: self.store,
|
|
1141
|
-
isEnabled: self.isEnabled(provider),
|
|
1142
|
-
subtitle: self.subtitle(provider),
|
|
1143
|
-
usageStatus: self.usageStatus(provider),
|
|
1144
|
-
sourceLabel: self.sourceLabel(provider),
|
|
1145
|
-
statusLabel: self.statusLabel(provider),
|
|
1146
|
-
errorDisplay: self.isEnabled(provider).wrappedValue ? self.errorDisplay(provider) : nil,
|
|
1147
|
-
isErrorExpanded: self.isErrorExpanded(provider),
|
|
1148
|
-
onCopyError: self.onCopyError)
|
|
1149
|
-
.padding(.bottom, showDividerOnProviderRow ? ProviderListMetrics.dividerBottomInset : 0)
|
|
1150
|
-
.listRowInsets(self.rowInsets(
|
|
1151
|
-
withDivider: showDividerOnProviderRow,
|
|
1152
|
-
addTopPadding: isFirstProvider,
|
|
1153
|
-
addBottomPadding: providerAddsBottomPadding))
|
|
1154
|
-
.listRowSeparator(.hidden)
|
|
1155
|
-
.providerSectionDivider(isVisible: showDividerOnProviderRow)
|
|
1156
|
-
|
|
1157
|
-
if isEnabled {
|
|
1158
|
-
let lastFieldID = fields.last?.id
|
|
1159
|
-
ForEach(fields) { field in
|
|
1160
|
-
let isLastField = field.id == lastFieldID
|
|
1161
|
-
let showDivider = shouldShowDivider && toggles.isEmpty && isLastField
|
|
1162
|
-
let fieldAddsBottomPadding = isLastProvider && toggles.isEmpty && isLastField
|
|
1163
|
-
|
|
1164
|
-
ProviderListFieldRowView(provider: provider, field: field)
|
|
1165
|
-
.id(self.rowID(provider: provider, suffix: field.id))
|
|
1166
|
-
.padding(.bottom, showDivider ? ProviderListMetrics.dividerBottomInset : 0)
|
|
1167
|
-
.listRowInsets(self.rowInsets(
|
|
1168
|
-
withDivider: showDivider,
|
|
1169
|
-
addTopPadding: false,
|
|
1170
|
-
addBottomPadding: fieldAddsBottomPadding))
|
|
1171
|
-
.listRowSeparator(.hidden)
|
|
1172
|
-
.providerSectionDivider(isVisible: showDivider)
|
|
1173
|
-
}
|
|
1174
|
-
let lastToggleID = toggles.last?.id
|
|
1175
|
-
ForEach(toggles) { toggle in
|
|
1176
|
-
let isLastToggle = toggle.id == lastToggleID
|
|
1177
|
-
let showDivider = shouldShowDivider && isLastToggle
|
|
1178
|
-
let toggleAddsBottomPadding = isLastProvider && isLastToggle
|
|
1179
|
-
|
|
1180
|
-
ProviderListToggleRowView(provider: provider, toggle: toggle)
|
|
1181
|
-
.id(self.rowID(provider: provider, suffix: toggle.id))
|
|
1182
|
-
.padding(.bottom, showDivider ? ProviderListMetrics.dividerBottomInset : 0)
|
|
1183
|
-
.listRowInsets(self.rowInsets(
|
|
1184
|
-
withDivider: showDivider,
|
|
1185
|
-
addTopPadding: false,
|
|
1186
|
-
addBottomPadding: toggleAddsBottomPadding))
|
|
1187
|
-
.listRowSeparator(.hidden)
|
|
1188
|
-
.providerSectionDivider(isVisible: showDivider)
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
.onMove { fromOffsets, toOffset in
|
|
1193
|
-
self.moveProviders(fromOffsets, toOffset)
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
|
1197
|
-
.listStyle(.plain)
|
|
1198
|
-
.scrollContentBackground(.hidden)
|
|
1199
|
-
.background(ProviderListScrollInsetFixer())
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
private func rowInsets(withDivider: Bool, addTopPadding: Bool, addBottomPadding: Bool) -> EdgeInsets {
|
|
1203
|
-
let base = ProviderListMetrics.rowInsets
|
|
1204
|
-
let topInset = addTopPadding ? ProviderListMetrics.sectionEdgeInset : base.top
|
|
1205
|
-
let bottomInset = addBottomPadding
|
|
1206
|
-
? ProviderListMetrics.sectionEdgeInset
|
|
1207
|
-
: (withDivider ? ProviderListMetrics.dividerBottomInset : base.bottom)
|
|
1208
|
-
return EdgeInsets(
|
|
1209
|
-
top: topInset,
|
|
1210
|
-
leading: base.leading,
|
|
1211
|
-
bottom: bottomInset,
|
|
1212
|
-
trailing: base.trailing)
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
private func rowID(provider: UsageProvider, suffix: String) -> String {
|
|
1216
|
-
"\(provider.rawValue)-\(suffix)"
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
@MainActor
|
|
1221
|
-
private struct ProviderListBrandIcon: View {
|
|
1222
|
-
let provider: UsageProvider
|
|
1223
|
-
|
|
1224
|
-
var body: some View {
|
|
1225
|
-
Group {
|
|
1226
|
-
if let brand = ProviderBrandIcon.image(for: self.provider, size: ProviderListMetrics.iconSize) {
|
|
1227
|
-
Image(nsImage: brand)
|
|
1228
|
-
.resizable()
|
|
1229
|
-
.scaledToFit()
|
|
1230
|
-
.frame(width: ProviderListMetrics.iconSize, height: ProviderListMetrics.iconSize)
|
|
1231
|
-
} else {
|
|
1232
|
-
let descriptor = ProviderDescriptorRegistry.descriptor(for: self.provider)
|
|
1233
|
-
let initial = String(descriptor.metadata.displayName.prefix(1)).uppercased()
|
|
1234
|
-
let brandColor = Color(
|
|
1235
|
-
red: Double(descriptor.branding.color.red),
|
|
1236
|
-
green: Double(descriptor.branding.color.green),
|
|
1237
|
-
blue: Double(descriptor.branding.color.blue))
|
|
1238
|
-
ZStack {
|
|
1239
|
-
RoundedRectangle(cornerRadius: RunicCornerRadius.sm, style: .continuous)
|
|
1240
|
-
.fill(brandColor.opacity(0.18))
|
|
1241
|
-
Text(initial)
|
|
1242
|
-
.font(RunicFont.system(size: ProviderListMetrics.iconSize * 0.5, weight: .bold))
|
|
1243
|
-
.foregroundStyle(brandColor)
|
|
1244
|
-
}
|
|
1245
|
-
.frame(width: ProviderListMetrics.iconSize, height: ProviderListMetrics.iconSize)
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
.shadow(color: .black.opacity(0.1), radius: 2, y: 1)
|
|
1249
|
-
.accessibilityHidden(true)
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
@MainActor
|
|
1254
|
-
private struct ProviderInsightsView: View {
|
|
1255
|
-
let lines: [ProviderInsightLine]
|
|
1256
|
-
|
|
1257
|
-
var body: some View {
|
|
1258
|
-
LazyVGrid(
|
|
1259
|
-
columns: [
|
|
1260
|
-
GridItem(
|
|
1261
|
-
.adaptive(minimum: ProviderListMetrics.providerInsightsGridItemMinWidth),
|
|
1262
|
-
spacing: ProviderListMetrics.providerInsightsChipSpacing),
|
|
1263
|
-
],
|
|
1264
|
-
alignment: .leading,
|
|
1265
|
-
spacing: ProviderListMetrics.providerInsightsChipSpacing)
|
|
1266
|
-
{
|
|
1267
|
-
ForEach(self.lines) { line in
|
|
1268
|
-
ProviderInsightChip(line: line)
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
.padding(ProviderListMetrics.insightsCardPadding)
|
|
1272
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1273
|
-
.background(
|
|
1274
|
-
RoundedRectangle(
|
|
1275
|
-
cornerRadius: ProviderListMetrics.providerInsightsCardCornerRadius,
|
|
1276
|
-
style: .continuous)
|
|
1277
|
-
.fill(Color(nsColor: .controlBackgroundColor).opacity(0.42)))
|
|
1278
|
-
.overlay {
|
|
1279
|
-
RoundedRectangle(
|
|
1280
|
-
cornerRadius: ProviderListMetrics.providerInsightsCardCornerRadius,
|
|
1281
|
-
style: .continuous)
|
|
1282
|
-
.strokeBorder(Color(nsColor: .separatorColor).opacity(0.22), lineWidth: 1)
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
private struct ProviderInsightChip: View {
|
|
1288
|
-
let line: ProviderInsightLine
|
|
1289
|
-
|
|
1290
|
-
var body: some View {
|
|
1291
|
-
VStack(alignment: .leading, spacing: RunicSpacing.xxxs) {
|
|
1292
|
-
Text(self.line.label.uppercased())
|
|
1293
|
-
.font(RunicFont.caption2.weight(.semibold))
|
|
1294
|
-
.tracking(0.2)
|
|
1295
|
-
.foregroundStyle(.tertiary)
|
|
1296
|
-
Text(self.line.value)
|
|
1297
|
-
.font(RunicFont.caption)
|
|
1298
|
-
.foregroundStyle(.secondary)
|
|
1299
|
-
.lineLimit(2)
|
|
1300
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
1301
|
-
.textSelection(.enabled)
|
|
1302
|
-
.help(self.line.help ?? "")
|
|
1303
|
-
}
|
|
1304
|
-
.padding(ProviderListMetrics.providerInsightsChipPadding)
|
|
1305
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1306
|
-
.background(
|
|
1307
|
-
RoundedRectangle(cornerRadius: ProviderListMetrics.providerInsightsChipCornerRadius, style: .continuous)
|
|
1308
|
-
.fill(Color(nsColor: .textBackgroundColor).opacity(0.45)))
|
|
1309
|
-
.overlay(
|
|
1310
|
-
RoundedRectangle(cornerRadius: ProviderListMetrics.providerInsightsChipCornerRadius, style: .continuous)
|
|
1311
|
-
.strokeBorder(Color(nsColor: .separatorColor).opacity(0.2), lineWidth: 1))
|
|
1312
|
-
.help(self.line.help ?? "")
|
|
1313
|
-
.accessibilityLabel("\(self.line.label): \(self.line.value)")
|
|
1314
|
-
.accessibilityHint(self.line.help ?? "")
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
@MainActor
|
|
1319
|
-
private struct ProviderListProviderRowView: View {
|
|
1320
|
-
let provider: UsageProvider
|
|
1321
|
-
@Bindable var store: UsageStore
|
|
1322
|
-
@Binding var isEnabled: Bool
|
|
1323
|
-
let subtitle: String
|
|
1324
|
-
let usageStatus: ProviderUsageStatus
|
|
1325
|
-
let sourceLabel: String
|
|
1326
|
-
let statusLabel: String
|
|
1327
|
-
let errorDisplay: ProviderErrorDisplay?
|
|
1328
|
-
@Binding var isErrorExpanded: Bool
|
|
1329
|
-
let onCopyError: (String) -> Void
|
|
1330
|
-
@State private var isHovering = false
|
|
1331
|
-
@FocusState private var isToggleFocused: Bool
|
|
1332
|
-
|
|
1333
|
-
var body: some View {
|
|
1334
|
-
let isRefreshing = self.store.refreshingProviders.contains(self.provider)
|
|
1335
|
-
let showReorderHandle = self.isHovering || self.isToggleFocused
|
|
1336
|
-
let metadata = self.store.metadata(for: self.provider)
|
|
1337
|
-
let insightLines = ProviderInsightsComposer.lines(for: self.provider, store: self.store, maxRows: 4)
|
|
1338
|
-
|
|
1339
|
-
HStack(alignment: .top, spacing: ProviderListMetrics.rowSpacing) {
|
|
1340
|
-
Toggle("", isOn: self.$isEnabled)
|
|
1341
|
-
.labelsHidden()
|
|
1342
|
-
.toggleStyle(.checkbox)
|
|
1343
|
-
.alignmentGuide(.top) { d in d[VerticalAlignment.center] }
|
|
1344
|
-
.focused(self.$isToggleFocused)
|
|
1345
|
-
|
|
1346
|
-
VStack(alignment: .leading, spacing: RunicSpacing.sm) {
|
|
1347
|
-
VStack(alignment: .leading, spacing: RunicSpacing.sm) {
|
|
1348
|
-
HStack(alignment: .top, spacing: RunicSpacing.sm) {
|
|
1349
|
-
ProviderListBrandIcon(provider: self.provider)
|
|
1350
|
-
VStack(alignment: .leading, spacing: RunicSpacing.xxs) {
|
|
1351
|
-
Text(metadata.displayName)
|
|
1352
|
-
.font(RunicFont.headline.weight(.semibold))
|
|
1353
|
-
.foregroundStyle(.primary)
|
|
1354
|
-
Text(self.subtitle)
|
|
1355
|
-
.font(RunicFont.footnote)
|
|
1356
|
-
.foregroundStyle(.secondary)
|
|
1357
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
1358
|
-
}
|
|
1359
|
-
Spacer(minLength: RunicSpacing.xs)
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
HStack(alignment: .center, spacing: RunicSpacing.xs) {
|
|
1363
|
-
self.sourceBadge
|
|
1364
|
-
Text(self.statusLabel)
|
|
1365
|
-
.font(RunicFont.caption2)
|
|
1366
|
-
.foregroundStyle(.tertiary)
|
|
1367
|
-
.lineLimit(1)
|
|
1368
|
-
|
|
1369
|
-
if isRefreshing {
|
|
1370
|
-
ProgressView()
|
|
1371
|
-
.controlSize(.small)
|
|
1372
|
-
.alignmentGuide(.firstTextBaseline) { d in d[VerticalAlignment.center] }
|
|
1373
|
-
Text("Refreshing…")
|
|
1374
|
-
.font(RunicFont.caption2)
|
|
1375
|
-
.foregroundStyle(.secondary)
|
|
1376
|
-
} else {
|
|
1377
|
-
self.usageStatusBadge
|
|
1378
|
-
.alignmentGuide(.firstTextBaseline) { d in d[VerticalAlignment.center] }
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
.contentShape(Rectangle())
|
|
1383
|
-
.onTapGesture { self.isEnabled.toggle() }
|
|
1384
|
-
|
|
1385
|
-
if !insightLines.isEmpty {
|
|
1386
|
-
ProviderInsightsView(lines: insightLines)
|
|
1387
|
-
.padding(.top, RunicSpacing.xxs)
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
if let errorDisplay {
|
|
1391
|
-
ProviderErrorView(
|
|
1392
|
-
title: "Last \(metadata.displayName) fetch failed:",
|
|
1393
|
-
display: errorDisplay,
|
|
1394
|
-
isExpanded: self.$isErrorExpanded,
|
|
1395
|
-
onCopy: { self.onCopyError(errorDisplay.full) })
|
|
1396
|
-
.padding(.top, RunicSpacing.xxs)
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1400
|
-
}
|
|
1401
|
-
.padding(ProviderListMetrics.providerCardPadding)
|
|
1402
|
-
.background(self.cardBackground)
|
|
1403
|
-
.overlay {
|
|
1404
|
-
RoundedRectangle(cornerRadius: ProviderListMetrics.providerCardCornerRadius, style: .continuous)
|
|
1405
|
-
.strokeBorder(self.cardBorderColor, lineWidth: 1)
|
|
1406
|
-
}
|
|
1407
|
-
.overlay(alignment: .topLeading) {
|
|
1408
|
-
ProviderListReorderHandle(isVisible: showReorderHandle)
|
|
1409
|
-
.offset(
|
|
1410
|
-
x: -(ProviderListMetrics.reorderHandleSize + RunicSpacing.xs),
|
|
1411
|
-
y: RunicSpacing.sm)
|
|
1412
|
-
}
|
|
1413
|
-
.onHover { isHovering in
|
|
1414
|
-
self.isHovering = isHovering
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
private var usageStatusBadge: some View {
|
|
1419
|
-
let (color, backgroundColor) = self.usageStatusColors
|
|
1420
|
-
|
|
1421
|
-
return Text(self.usageStatus.text)
|
|
1422
|
-
.font(RunicFont.caption2.weight(.medium))
|
|
1423
|
-
.padding(.horizontal, ProviderListMetrics.statusBadgePaddingH)
|
|
1424
|
-
.padding(.vertical, ProviderListMetrics.statusBadgePaddingV)
|
|
1425
|
-
.background(Capsule(style: .continuous).fill(backgroundColor))
|
|
1426
|
-
.foregroundStyle(color)
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
private var sourceBadge: some View {
|
|
1430
|
-
Text(self.sourceLabel)
|
|
1431
|
-
.font(RunicFont.caption2.weight(.medium))
|
|
1432
|
-
.foregroundStyle(.secondary)
|
|
1433
|
-
.padding(.horizontal, RunicSpacing.xs)
|
|
1434
|
-
.padding(.vertical, RunicSpacing.xxxs)
|
|
1435
|
-
.background(
|
|
1436
|
-
Capsule(style: .continuous)
|
|
1437
|
-
.fill(Color(nsColor: .controlBackgroundColor).opacity(0.7)))
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
private var usageStatusColors: (Color, Color) {
|
|
1441
|
-
switch self.usageStatus.style {
|
|
1442
|
-
case .success:
|
|
1443
|
-
(.green, Color.green.opacity(0.15))
|
|
1444
|
-
case .error:
|
|
1445
|
-
(.red, Color.red.opacity(0.15))
|
|
1446
|
-
case .neutral:
|
|
1447
|
-
(.secondary, Color(nsColor: .controlBackgroundColor))
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
private var rowBackgroundColor: Color {
|
|
1452
|
-
if self.isHovering {
|
|
1453
|
-
return Color(nsColor: .controlBackgroundColor).opacity(0.72)
|
|
1454
|
-
} else if self.isEnabled {
|
|
1455
|
-
return Color(nsColor: .controlBackgroundColor).opacity(ProviderListMetrics.providerCardBackgroundOpacity)
|
|
1456
|
-
}
|
|
1457
|
-
return Color(nsColor: .controlBackgroundColor).opacity(0.35)
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
private var cardBorderColor: Color {
|
|
1461
|
-
if self.isHovering {
|
|
1462
|
-
return Color.accentColor.opacity(0.35)
|
|
1463
|
-
}
|
|
1464
|
-
if self.isEnabled {
|
|
1465
|
-
return Color(nsColor: .separatorColor).opacity(ProviderListMetrics.providerCardBorderOpacity)
|
|
1466
|
-
}
|
|
1467
|
-
return Color(nsColor: .separatorColor).opacity(0.18)
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
private var cardBackground: some View {
|
|
1471
|
-
RoundedRectangle(cornerRadius: ProviderListMetrics.providerCardCornerRadius, style: .continuous)
|
|
1472
|
-
.fill(self.rowBackgroundColor)
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
@MainActor
|
|
1477
|
-
private struct ProviderListReorderHandle: View {
|
|
1478
|
-
let isVisible: Bool
|
|
1479
|
-
|
|
1480
|
-
var body: some View {
|
|
1481
|
-
VStack(spacing: ProviderListMetrics.reorderDotSpacing) {
|
|
1482
|
-
ForEach(0..<3, id: \.self) { _ in
|
|
1483
|
-
HStack(spacing: ProviderListMetrics.reorderDotSpacing) {
|
|
1484
|
-
Circle()
|
|
1485
|
-
.frame(
|
|
1486
|
-
width: ProviderListMetrics.reorderDotSize,
|
|
1487
|
-
height: ProviderListMetrics.reorderDotSize)
|
|
1488
|
-
Circle()
|
|
1489
|
-
.frame(
|
|
1490
|
-
width: ProviderListMetrics.reorderDotSize,
|
|
1491
|
-
height: ProviderListMetrics.reorderDotSize)
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
.frame(width: ProviderListMetrics.reorderHandleSize, height: ProviderListMetrics.reorderHandleSize)
|
|
1496
|
-
.foregroundStyle(.tertiary)
|
|
1497
|
-
.opacity(self.isVisible ? 1 : 0)
|
|
1498
|
-
.animation(.easeInOut(duration: 0.15), value: self.isVisible)
|
|
1499
|
-
.help("Drag to reorder")
|
|
1500
|
-
.accessibilityHidden(true)
|
|
1501
|
-
.allowsHitTesting(false)
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
@MainActor
|
|
1506
|
-
private struct ProviderListSectionDividerView: View {
|
|
1507
|
-
var body: some View {
|
|
1508
|
-
Rectangle()
|
|
1509
|
-
.fill(Color(nsColor: .separatorColor).opacity(0.5))
|
|
1510
|
-
.frame(height: 1)
|
|
1511
|
-
.padding(.leading, ProviderListMetrics.dividerLeadingInset)
|
|
1512
|
-
.padding(.trailing, ProviderListMetrics.dividerTrailingInset)
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
extension View {
|
|
1517
|
-
fileprivate func providerSectionDivider(isVisible: Bool) -> some View {
|
|
1518
|
-
overlay(alignment: .bottom) {
|
|
1519
|
-
if isVisible {
|
|
1520
|
-
ProviderListSectionDividerView()
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
@MainActor
|
|
1527
|
-
private struct ProviderListToggleRowView: View {
|
|
1528
|
-
let provider: UsageProvider
|
|
1529
|
-
let toggle: ProviderSettingsToggleDescriptor
|
|
1530
|
-
|
|
1531
|
-
var body: some View {
|
|
1532
|
-
HStack(alignment: .top, spacing: ProviderListMetrics.rowSpacing) {
|
|
1533
|
-
Toggle("", isOn: self.toggle.binding)
|
|
1534
|
-
.labelsHidden()
|
|
1535
|
-
.toggleStyle(.checkbox)
|
|
1536
|
-
.alignmentGuide(.top) { d in d[VerticalAlignment.center] }
|
|
1537
|
-
|
|
1538
|
-
VStack(alignment: .leading, spacing: RunicSpacing.sm) {
|
|
1539
|
-
VStack(alignment: .leading, spacing: RunicSpacing.xxs) {
|
|
1540
|
-
Text(self.toggle.title)
|
|
1541
|
-
.font(RunicFont.callout.weight(.semibold))
|
|
1542
|
-
Text(self.toggle.subtitle)
|
|
1543
|
-
.font(RunicFont.footnote)
|
|
1544
|
-
.foregroundStyle(.secondary)
|
|
1545
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
if self.toggle.binding.wrappedValue {
|
|
1549
|
-
if let status = self.toggle.statusText?(), !status.isEmpty {
|
|
1550
|
-
Text(status)
|
|
1551
|
-
.font(RunicFont.caption)
|
|
1552
|
-
.foregroundStyle(.secondary)
|
|
1553
|
-
.lineLimit(4)
|
|
1554
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
1555
|
-
.padding(RunicSpacing.xs)
|
|
1556
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1557
|
-
.background(
|
|
1558
|
-
RoundedRectangle(cornerRadius: RunicCornerRadius.sm, style: .continuous)
|
|
1559
|
-
.fill(Color(nsColor: .controlBackgroundColor).opacity(0.55)))
|
|
1560
|
-
.overlay {
|
|
1561
|
-
RoundedRectangle(cornerRadius: RunicCornerRadius.sm, style: .continuous)
|
|
1562
|
-
.strokeBorder(Color(nsColor: .separatorColor).opacity(0.2), lineWidth: 1)
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
let actions = self.toggle.actions.filter { $0.isVisible?() ?? true }
|
|
1567
|
-
if !actions.isEmpty {
|
|
1568
|
-
HStack(spacing: RunicSpacing.xs) {
|
|
1569
|
-
ForEach(actions) { action in
|
|
1570
|
-
Button(action.title) {
|
|
1571
|
-
Task { @MainActor in
|
|
1572
|
-
await action.perform()
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
.applyProviderSettingsButtonStyle(action.style)
|
|
1576
|
-
.controlSize(.small)
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
.padding(.leading, ProviderListMetrics.iconSize + RunicSpacing.sm)
|
|
1583
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1584
|
-
}
|
|
1585
|
-
.padding(ProviderListMetrics.supplementalCardPadding)
|
|
1586
|
-
.background(self.supplementalCardBackground)
|
|
1587
|
-
.overlay {
|
|
1588
|
-
RoundedRectangle(cornerRadius: ProviderListMetrics.providerCardCornerRadius, style: .continuous)
|
|
1589
|
-
.strokeBorder(self.supplementalCardBorderColor, lineWidth: 1)
|
|
1590
|
-
}
|
|
1591
|
-
.onChange(of: self.toggle.binding.wrappedValue) { _, enabled in
|
|
1592
|
-
guard let onChange = self.toggle.onChange else { return }
|
|
1593
|
-
Task { @MainActor in
|
|
1594
|
-
await onChange(enabled)
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
.task(id: self.toggle.binding.wrappedValue) {
|
|
1598
|
-
guard self.toggle.binding.wrappedValue else { return }
|
|
1599
|
-
guard let onAppear = self.toggle.onAppearWhenEnabled else { return }
|
|
1600
|
-
await onAppear()
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
private var supplementalCardBackground: some View {
|
|
1605
|
-
RoundedRectangle(cornerRadius: ProviderListMetrics.providerCardCornerRadius, style: .continuous)
|
|
1606
|
-
.fill(Color(nsColor: .controlBackgroundColor)
|
|
1607
|
-
.opacity(ProviderListMetrics.supplementalCardBackgroundOpacity))
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
private var supplementalCardBorderColor: Color {
|
|
1611
|
-
Color(nsColor: .separatorColor).opacity(ProviderListMetrics.supplementalCardBorderOpacity)
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
@MainActor
|
|
1616
|
-
private struct ProviderListFieldRowView: View {
|
|
1617
|
-
let provider: UsageProvider
|
|
1618
|
-
let field: ProviderSettingsFieldDescriptor
|
|
1619
|
-
|
|
1620
|
-
var body: some View {
|
|
1621
|
-
HStack(alignment: .top, spacing: ProviderListMetrics.rowSpacing) {
|
|
1622
|
-
Color.clear
|
|
1623
|
-
.frame(width: ProviderListMetrics.checkboxSize, height: ProviderListMetrics.checkboxSize)
|
|
1624
|
-
.alignmentGuide(.top) { d in d[VerticalAlignment.center] }
|
|
1625
|
-
|
|
1626
|
-
VStack(alignment: .leading, spacing: RunicSpacing.sm) {
|
|
1627
|
-
VStack(alignment: .leading, spacing: RunicSpacing.xxs) {
|
|
1628
|
-
Text(self.field.title)
|
|
1629
|
-
.font(RunicFont.callout.weight(.semibold))
|
|
1630
|
-
Text(self.field.subtitle)
|
|
1631
|
-
.font(RunicFont.footnote)
|
|
1632
|
-
.foregroundStyle(.secondary)
|
|
1633
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
switch self.field.kind {
|
|
1637
|
-
case .plain:
|
|
1638
|
-
TextField(self.field.placeholder ?? "", text: self.field.binding)
|
|
1639
|
-
.textFieldStyle(.roundedBorder)
|
|
1640
|
-
.font(RunicFont.callout)
|
|
1641
|
-
.frame(maxWidth: ProviderListMetrics.fieldMaxWidth, alignment: .leading)
|
|
1642
|
-
case .secure:
|
|
1643
|
-
SecureField(self.field.placeholder ?? "", text: self.field.binding)
|
|
1644
|
-
.textFieldStyle(.roundedBorder)
|
|
1645
|
-
.font(RunicFont.callout)
|
|
1646
|
-
.frame(maxWidth: ProviderListMetrics.fieldMaxWidth, alignment: .leading)
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
let actions = self.field.actions.filter { $0.isVisible?() ?? true }
|
|
1650
|
-
if !actions.isEmpty {
|
|
1651
|
-
HStack(spacing: RunicSpacing.xs) {
|
|
1652
|
-
ForEach(actions) { action in
|
|
1653
|
-
Button(action.title) {
|
|
1654
|
-
Task { @MainActor in
|
|
1655
|
-
await action.perform()
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
.applyProviderSettingsButtonStyle(action.style)
|
|
1659
|
-
.controlSize(.small)
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
.padding(.leading, ProviderListMetrics.iconSize + RunicSpacing.sm)
|
|
1665
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1666
|
-
}
|
|
1667
|
-
.padding(ProviderListMetrics.supplementalCardPadding)
|
|
1668
|
-
.background(self.supplementalCardBackground)
|
|
1669
|
-
.overlay {
|
|
1670
|
-
RoundedRectangle(cornerRadius: ProviderListMetrics.providerCardCornerRadius, style: .continuous)
|
|
1671
|
-
.strokeBorder(self.supplementalCardBorderColor, lineWidth: 1)
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
private var supplementalCardBackground: some View {
|
|
1676
|
-
RoundedRectangle(cornerRadius: ProviderListMetrics.providerCardCornerRadius, style: .continuous)
|
|
1677
|
-
.fill(Color(nsColor: .controlBackgroundColor)
|
|
1678
|
-
.opacity(ProviderListMetrics.supplementalCardBackgroundOpacity))
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
private var supplementalCardBorderColor: Color {
|
|
1682
|
-
Color(nsColor: .separatorColor).opacity(0.18)
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
extension View {
|
|
1687
|
-
@ViewBuilder
|
|
1688
|
-
fileprivate func applyProviderSettingsButtonStyle(_ style: ProviderSettingsActionDescriptor.Style) -> some View {
|
|
1689
|
-
switch style {
|
|
1690
|
-
case .bordered:
|
|
1691
|
-
self.buttonStyle(.bordered)
|
|
1692
|
-
case .link:
|
|
1693
|
-
self.buttonStyle(.link)
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
private struct ProviderErrorDisplay {
|
|
1699
|
-
let preview: String
|
|
1700
|
-
let full: String
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
@MainActor
|
|
1704
|
-
private struct ProviderListScrollInsetFixer: NSViewRepresentable {
|
|
1705
|
-
private final class HitTestIgnoringView: NSView {
|
|
1706
|
-
override func hitTest(_ point: NSPoint) -> NSView? {
|
|
1707
|
-
nil
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
func makeNSView(context: Context) -> NSView {
|
|
1712
|
-
HitTestIgnoringView()
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
func updateNSView(_ nsView: NSView, context: Context) {
|
|
1716
|
-
Task { @MainActor in
|
|
1717
|
-
guard let scrollView = Self.findScrollView(from: nsView) else { return }
|
|
1718
|
-
if scrollView.automaticallyAdjustsContentInsets {
|
|
1719
|
-
scrollView.automaticallyAdjustsContentInsets = false
|
|
1720
|
-
}
|
|
1721
|
-
let zeroInsets = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
|
1722
|
-
let currentContentInsets = scrollView.contentInsets
|
|
1723
|
-
if currentContentInsets.top != 0 || currentContentInsets.left != 0 ||
|
|
1724
|
-
currentContentInsets.bottom != 0 || currentContentInsets.right != 0
|
|
1725
|
-
{
|
|
1726
|
-
scrollView.contentInsets = zeroInsets
|
|
1727
|
-
}
|
|
1728
|
-
let currentScrollerInsets = scrollView.scrollerInsets
|
|
1729
|
-
if currentScrollerInsets.top != 0 || currentScrollerInsets.left != 0 ||
|
|
1730
|
-
currentScrollerInsets.bottom != 0 || currentScrollerInsets.right != 0
|
|
1731
|
-
{
|
|
1732
|
-
scrollView.scrollerInsets = zeroInsets
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
private static func findScrollView(from view: NSView) -> NSScrollView? {
|
|
1738
|
-
var current: NSView? = view
|
|
1739
|
-
while let candidate = current {
|
|
1740
|
-
if let scroll = candidate as? NSScrollView { return scroll }
|
|
1741
|
-
if let found = candidate.subviews.compactMap({ $0 as? NSScrollView }).first {
|
|
1742
|
-
return found
|
|
1743
|
-
}
|
|
1744
|
-
current = candidate.superview
|
|
1745
|
-
}
|
|
1746
|
-
return nil
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
@MainActor
|
|
1751
|
-
private struct ProviderErrorView: View {
|
|
1752
|
-
let title: String
|
|
1753
|
-
let display: ProviderErrorDisplay
|
|
1754
|
-
@Binding var isExpanded: Bool
|
|
1755
|
-
let onCopy: () -> Void
|
|
1756
|
-
|
|
1757
|
-
var body: some View {
|
|
1758
|
-
VStack(alignment: .leading, spacing: RunicSpacing.xs) {
|
|
1759
|
-
HStack(alignment: .center, spacing: RunicSpacing.xs) {
|
|
1760
|
-
Image(systemName: "exclamationmark.triangle.fill")
|
|
1761
|
-
.font(RunicFont.caption)
|
|
1762
|
-
.foregroundStyle(.orange)
|
|
1763
|
-
.alignmentGuide(.firstTextBaseline) { d in d[VerticalAlignment.center] }
|
|
1764
|
-
Text(self.title)
|
|
1765
|
-
.font(RunicFont.callout.weight(.semibold))
|
|
1766
|
-
.foregroundStyle(.orange)
|
|
1767
|
-
Spacer()
|
|
1768
|
-
Button {
|
|
1769
|
-
self.onCopy()
|
|
1770
|
-
} label: {
|
|
1771
|
-
HStack(alignment: .center, spacing: RunicSpacing.xxs) {
|
|
1772
|
-
Image(systemName: "doc.on.doc")
|
|
1773
|
-
.font(RunicFont.caption)
|
|
1774
|
-
Text("Copy")
|
|
1775
|
-
.font(RunicFont.caption)
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
.buttonStyle(.bordered)
|
|
1779
|
-
.controlSize(.small)
|
|
1780
|
-
.help("Copy error to clipboard")
|
|
1781
|
-
.alignmentGuide(.firstTextBaseline) { d in d[VerticalAlignment.center] }
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
Text(self.display.preview)
|
|
1785
|
-
.font(RunicFont.callout)
|
|
1786
|
-
.foregroundStyle(.secondary)
|
|
1787
|
-
.lineLimit(3)
|
|
1788
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
1789
|
-
.padding(RunicSpacing.xs)
|
|
1790
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1791
|
-
.background(
|
|
1792
|
-
RoundedRectangle(cornerRadius: ProviderListMetrics.errorCardCornerRadius, style: .continuous)
|
|
1793
|
-
.fill(Color.orange.opacity(0.08)))
|
|
1794
|
-
|
|
1795
|
-
if self.display.preview != self.display.full {
|
|
1796
|
-
Button(self.isExpanded ? "Hide details" : "Show details") { self.isExpanded.toggle() }
|
|
1797
|
-
.buttonStyle(.link)
|
|
1798
|
-
.font(RunicFont.callout)
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
if self.isExpanded {
|
|
1802
|
-
ScrollView(.horizontal, showsIndicators: true) {
|
|
1803
|
-
Text(self.display.full)
|
|
1804
|
-
.font(RunicFont.caption)
|
|
1805
|
-
.foregroundStyle(.primary)
|
|
1806
|
-
.textSelection(.enabled)
|
|
1807
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
1808
|
-
.padding(ProviderListMetrics.errorCardPadding)
|
|
1809
|
-
}
|
|
1810
|
-
.frame(maxHeight: 200)
|
|
1811
|
-
.background(
|
|
1812
|
-
RoundedRectangle(cornerRadius: ProviderListMetrics.errorCardCornerRadius, style: .continuous)
|
|
1813
|
-
.fill(Color(nsColor: .textBackgroundColor)))
|
|
1814
|
-
.overlay(
|
|
1815
|
-
RoundedRectangle(cornerRadius: ProviderListMetrics.errorCardCornerRadius, style: .continuous)
|
|
1816
|
-
.stroke(Color.orange.opacity(0.3), lineWidth: 1))
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
.padding(ProviderListMetrics.errorCardPadding)
|
|
1820
|
-
.background(
|
|
1821
|
-
RoundedRectangle(cornerRadius: ProviderListMetrics.errorCardCornerRadius, style: .continuous)
|
|
1822
|
-
.fill(Color.orange.opacity(0.03)))
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
@MainActor
|
|
1827
|
-
private struct ProviderSettingsConfirmationState: Identifiable {
|
|
1828
|
-
let id = UUID()
|
|
1829
|
-
let title: String
|
|
1830
|
-
let message: String
|
|
1831
|
-
let confirmTitle: String
|
|
1832
|
-
let onConfirm: () -> Void
|
|
1833
|
-
|
|
1834
|
-
init(confirmation: ProviderSettingsConfirmation) {
|
|
1835
|
-
self.title = confirmation.title
|
|
1836
|
-
self.message = confirmation.message
|
|
1837
|
-
self.confirmTitle = confirmation.confirmTitle
|
|
1838
|
-
self.onConfirm = confirmation.onConfirm
|
|
1839
|
-
}
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
// MARK: - Sidebar layout views
|
|
1843
|
-
|
|
1844
|
-
@MainActor
|
|
1845
|
-
private struct ProviderSidebarRow: View {
|
|
1846
|
-
let provider: UsageProvider
|
|
1847
|
-
@Bindable var store: UsageStore
|
|
1848
|
-
let isEnabled: Bool
|
|
1849
|
-
let isSelected: Bool
|
|
1850
|
-
|
|
1851
|
-
var body: some View {
|
|
1852
|
-
HStack(spacing: RunicSpacing.xs) {
|
|
1853
|
-
if let brand = ProviderBrandIcon.image(for: self.provider, size: 20) {
|
|
1854
|
-
Image(nsImage: brand)
|
|
1855
|
-
.resizable()
|
|
1856
|
-
.scaledToFit()
|
|
1857
|
-
.frame(width: 20, height: 20)
|
|
1858
|
-
.foregroundStyle(self.isEnabled ? .primary : .tertiary)
|
|
1859
|
-
} else {
|
|
1860
|
-
Image(systemName: "circle.dotted")
|
|
1861
|
-
.font(.system(size: 16))
|
|
1862
|
-
.foregroundStyle(.tertiary)
|
|
1863
|
-
.frame(width: 20, height: 20)
|
|
1864
|
-
}
|
|
1865
|
-
Text(self.store.metadata(for: self.provider).displayName)
|
|
1866
|
-
.font(RunicFont.body)
|
|
1867
|
-
.foregroundStyle(self.isEnabled ? .primary : .secondary)
|
|
1868
|
-
.lineLimit(1)
|
|
1869
|
-
.layoutPriority(1)
|
|
1870
|
-
}
|
|
1871
|
-
.padding(.horizontal, RunicSpacing.xs)
|
|
1872
|
-
.padding(.vertical, RunicSpacing.sm)
|
|
1873
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1874
|
-
.background(
|
|
1875
|
-
RoundedRectangle(cornerRadius: RunicCornerRadius.sm, style: .continuous)
|
|
1876
|
-
.fill(self.isSelected ? Color.accentColor.opacity(0.14) : .clear))
|
|
1877
|
-
.overlay(
|
|
1878
|
-
RoundedRectangle(cornerRadius: RunicCornerRadius.sm, style: .continuous)
|
|
1879
|
-
.stroke(
|
|
1880
|
-
self.isSelected ? Color.accentColor.opacity(0.45) : Color.clear,
|
|
1881
|
-
lineWidth: 1))
|
|
1882
|
-
.contentShape(Rectangle())
|
|
1883
|
-
.opacity(self.isEnabled ? 1 : 0.6)
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
private struct ProviderSidebarSectionCard<Content: View>: View {
|
|
1888
|
-
private let content: Content
|
|
1889
|
-
|
|
1890
|
-
init(@ViewBuilder content: () -> Content) {
|
|
1891
|
-
self.content = content()
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
var body: some View {
|
|
1895
|
-
self.content
|
|
1896
|
-
.padding(ProviderListMetrics.sidebarCardPadding)
|
|
1897
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
1898
|
-
.background(
|
|
1899
|
-
RoundedRectangle(
|
|
1900
|
-
cornerRadius: ProviderListMetrics.sidebarCardCornerRadius,
|
|
1901
|
-
style: .continuous)
|
|
1902
|
-
.fill(Color(nsColor: .controlBackgroundColor)
|
|
1903
|
-
.opacity(ProviderListMetrics.sidebarCardBackgroundOpacity)))
|
|
1904
|
-
.overlay(
|
|
1905
|
-
RoundedRectangle(
|
|
1906
|
-
cornerRadius: ProviderListMetrics.sidebarCardCornerRadius,
|
|
1907
|
-
style: .continuous)
|
|
1908
|
-
.strokeBorder(
|
|
1909
|
-
Color(nsColor: .separatorColor).opacity(ProviderListMetrics.sidebarCardBorderOpacity),
|
|
1910
|
-
lineWidth: 1))
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
private struct ProviderHistoryNavigationButton: View {
|
|
1915
|
-
let systemName: String
|
|
1916
|
-
let enabled: Bool
|
|
1917
|
-
let help: String
|
|
1918
|
-
let action: () -> Void
|
|
1919
|
-
|
|
1920
|
-
var body: some View {
|
|
1921
|
-
Button(action: self.action) {
|
|
1922
|
-
Image(systemName: self.systemName)
|
|
1923
|
-
.font(RunicFont.caption.weight(.semibold))
|
|
1924
|
-
.foregroundStyle(self.enabled ? .secondary : .tertiary)
|
|
1925
|
-
.frame(width: 26, height: 26)
|
|
1926
|
-
}
|
|
1927
|
-
.buttonStyle(.plain)
|
|
1928
|
-
.disabled(!self.enabled)
|
|
1929
|
-
.background(
|
|
1930
|
-
Circle()
|
|
1931
|
-
.fill(Color(nsColor: .controlBackgroundColor).opacity(self.enabled ? 0.48 : 0.2))
|
|
1932
|
-
.overlay(
|
|
1933
|
-
Circle()
|
|
1934
|
-
.strokeBorder(
|
|
1935
|
-
Color(
|
|
1936
|
-
nsColor: .separatorColor).opacity(self.enabled ? 0.28 : 0.1),
|
|
1937
|
-
lineWidth: 1)))
|
|
1938
|
-
.help(self.help)
|
|
1939
|
-
.accessibilityLabel(self.help)
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
private struct ProviderSidebarSectionHeader: View {
|
|
1944
|
-
let title: String
|
|
1945
|
-
|
|
1946
|
-
var body: some View {
|
|
1947
|
-
Text(self.title)
|
|
1948
|
-
.font(RunicFont.subheadline.weight(.semibold))
|
|
1949
|
-
.foregroundStyle(.secondary)
|
|
1950
|
-
}
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
@MainActor
|
|
1954
|
-
private struct ProviderSidebarKeyValueRow: View {
|
|
1955
|
-
let label: String
|
|
1956
|
-
let value: String
|
|
1957
|
-
let helpText: String?
|
|
1958
|
-
|
|
1959
|
-
var body: some View {
|
|
1960
|
-
HStack(alignment: .firstTextBaseline, spacing: RunicSpacing.xs) {
|
|
1961
|
-
Text("\(self.label):")
|
|
1962
|
-
.font(RunicFont.caption)
|
|
1963
|
-
.foregroundStyle(.tertiary)
|
|
1964
|
-
.frame(width: ProviderListMetrics.sidebarStatusLabelWidth, alignment: .leading)
|
|
1965
|
-
if let helpText = self.helpText {
|
|
1966
|
-
Text(self.value)
|
|
1967
|
-
.font(RunicFont.caption)
|
|
1968
|
-
.foregroundStyle(.secondary)
|
|
1969
|
-
.lineLimit(2)
|
|
1970
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
1971
|
-
.textSelection(.enabled)
|
|
1972
|
-
.help(helpText)
|
|
1973
|
-
} else {
|
|
1974
|
-
Text(self.value)
|
|
1975
|
-
.font(RunicFont.caption)
|
|
1976
|
-
.foregroundStyle(.secondary)
|
|
1977
|
-
.lineLimit(2)
|
|
1978
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
1979
|
-
.textSelection(.enabled)
|
|
1980
|
-
}
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
1983
|
-
}
|
|
1984
|
-
|
|
1985
|
-
@MainActor
|
|
1986
|
-
private struct ProviderSidebarMetricChip: View {
|
|
1987
|
-
let title: String
|
|
1988
|
-
let value: String
|
|
1989
|
-
let helpText: String?
|
|
1990
|
-
|
|
1991
|
-
var body: some View {
|
|
1992
|
-
VStack(alignment: .leading, spacing: RunicSpacing.xxxs) {
|
|
1993
|
-
Text(self.title)
|
|
1994
|
-
.font(RunicFont.caption2.weight(.semibold))
|
|
1995
|
-
.foregroundStyle(.tertiary)
|
|
1996
|
-
.textCase(.uppercase)
|
|
1997
|
-
Text(self.value)
|
|
1998
|
-
.font(RunicFont.caption.weight(.medium))
|
|
1999
|
-
.foregroundStyle(.secondary)
|
|
2000
|
-
.lineLimit(2)
|
|
2001
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
2002
|
-
.textSelection(.enabled)
|
|
2003
|
-
}
|
|
2004
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
2005
|
-
.padding(.horizontal, RunicSpacing.xs)
|
|
2006
|
-
.padding(.vertical, RunicSpacing.xs)
|
|
2007
|
-
.background(
|
|
2008
|
-
RoundedRectangle(
|
|
2009
|
-
cornerRadius: ProviderListMetrics.sidebarMicroCardCornerRadius,
|
|
2010
|
-
style: .continuous)
|
|
2011
|
-
.fill(Color(nsColor: .textBackgroundColor)
|
|
2012
|
-
.opacity(ProviderListMetrics.sidebarMicroCardBackgroundOpacity)))
|
|
2013
|
-
.overlay(
|
|
2014
|
-
RoundedRectangle(
|
|
2015
|
-
cornerRadius: ProviderListMetrics.sidebarMicroCardCornerRadius,
|
|
2016
|
-
style: .continuous)
|
|
2017
|
-
.strokeBorder(Color(
|
|
2018
|
-
nsColor: .separatorColor).opacity(ProviderListMetrics.sidebarMicroCardBorderOpacity), lineWidth: 1))
|
|
2019
|
-
.help(self.helpText ?? "")
|
|
2020
|
-
}
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
|
-
private enum ProviderDetailSubview: String, CaseIterable, Identifiable {
|
|
2024
|
-
case overview = "Overview"
|
|
2025
|
-
case history = "History"
|
|
2026
|
-
|
|
2027
|
-
var id: String {
|
|
2028
|
-
self.rawValue
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
private enum ProviderHistoryMetricMode: String, CaseIterable, Identifiable {
|
|
2033
|
-
case tokens = "Tokens"
|
|
2034
|
-
case cost = "Cost"
|
|
2035
|
-
case requests = "Requests"
|
|
2036
|
-
|
|
2037
|
-
var id: String {
|
|
2038
|
-
self.rawValue
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
|
|
2042
|
-
private enum ProviderHistoryDayDetailMode: String, CaseIterable, Identifiable {
|
|
2043
|
-
case summary = "Summary"
|
|
2044
|
-
case models = "Models"
|
|
2045
|
-
case projects = "Projects"
|
|
2046
|
-
|
|
2047
|
-
var id: String {
|
|
2048
|
-
self.rawValue
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
@MainActor
|
|
2053
|
-
private struct ProviderHistoryCalendarDayCell: View {
|
|
2054
|
-
let dayNumber: Int
|
|
2055
|
-
let isInMonth: Bool
|
|
2056
|
-
let isSelected: Bool
|
|
2057
|
-
let hasActivity: Bool
|
|
2058
|
-
let intensity: Double
|
|
2059
|
-
let action: () -> Void
|
|
2060
|
-
|
|
2061
|
-
var body: some View {
|
|
2062
|
-
Button(action: self.action) {
|
|
2063
|
-
VStack(alignment: .leading, spacing: RunicSpacing.xxxs) {
|
|
2064
|
-
Text("\(self.dayNumber)")
|
|
2065
|
-
.font(RunicFont.caption.weight(self.isSelected ? .semibold : .regular))
|
|
2066
|
-
.foregroundStyle(self.isInMonth ? .primary : .tertiary)
|
|
2067
|
-
Spacer(minLength: 0)
|
|
2068
|
-
if self.hasActivity {
|
|
2069
|
-
Capsule(style: .continuous)
|
|
2070
|
-
.fill(Color.accentColor.opacity(0.35 + (0.45 * self.intensity)))
|
|
2071
|
-
.frame(height: 4)
|
|
2072
|
-
} else {
|
|
2073
|
-
Capsule(style: .continuous)
|
|
2074
|
-
.fill(Color(nsColor: .separatorColor).opacity(self.isInMonth ? 0.14 : 0.08))
|
|
2075
|
-
.frame(height: 2)
|
|
2076
|
-
}
|
|
2077
|
-
}
|
|
2078
|
-
.padding(RunicSpacing.xs)
|
|
2079
|
-
.frame(maxWidth: .infinity, minHeight: 46, alignment: .topLeading)
|
|
2080
|
-
.background(
|
|
2081
|
-
RoundedRectangle(cornerRadius: RunicCornerRadius.sm, style: .continuous)
|
|
2082
|
-
.fill(self.backgroundColor))
|
|
2083
|
-
.overlay(
|
|
2084
|
-
RoundedRectangle(cornerRadius: RunicCornerRadius.sm, style: .continuous)
|
|
2085
|
-
.strokeBorder(self.borderColor, lineWidth: self.isSelected ? 1.5 : 1))
|
|
2086
|
-
.opacity(self.isInMonth ? 1 : 0.52)
|
|
2087
|
-
}
|
|
2088
|
-
.buttonStyle(.plain)
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
private var backgroundColor: Color {
|
|
2092
|
-
if self.isSelected {
|
|
2093
|
-
return Color.accentColor.opacity(0.18)
|
|
2094
|
-
}
|
|
2095
|
-
if self.hasActivity {
|
|
2096
|
-
return Color.accentColor.opacity(0.06 + (0.18 * self.intensity))
|
|
2097
|
-
}
|
|
2098
|
-
return Color(nsColor: .textBackgroundColor).opacity(self.isInMonth ? 0.55 : 0.32)
|
|
2099
|
-
}
|
|
2100
|
-
|
|
2101
|
-
private var borderColor: Color {
|
|
2102
|
-
if self.isSelected {
|
|
2103
|
-
return Color.accentColor.opacity(0.75)
|
|
2104
|
-
}
|
|
2105
|
-
return Color(nsColor: .separatorColor).opacity(self.isInMonth ? 0.22 : 0.12)
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
|
-
@MainActor
|
|
2110
|
-
private struct ProviderSidebarDetailView: View {
|
|
2111
|
-
let provider: UsageProvider
|
|
2112
|
-
@Bindable var store: UsageStore
|
|
2113
|
-
@Binding var isEnabled: Bool
|
|
2114
|
-
let subtitle: String
|
|
2115
|
-
let usageStatus: ProviderUsageStatus
|
|
2116
|
-
let sourceLabel: String
|
|
2117
|
-
let statusLabel: String
|
|
2118
|
-
let settingsToggles: [ProviderSettingsToggleDescriptor]
|
|
2119
|
-
let settingsFields: [ProviderSettingsFieldDescriptor]
|
|
2120
|
-
let errorDisplay: ProviderErrorDisplay?
|
|
2121
|
-
@Binding var isErrorExpanded: Bool
|
|
2122
|
-
let onCopyError: (String) -> Void
|
|
2123
|
-
@State private var diagnosticsCopyStatus: String?
|
|
2124
|
-
@State private var selectedSubview: ProviderDetailSubview = .overview
|
|
2125
|
-
@State private var historyMetricMode: ProviderHistoryMetricMode = .tokens
|
|
2126
|
-
@State private var historyMonthStart: Date = Self.monthStart(for: Date())
|
|
2127
|
-
@State private var historySnapshot: ProviderHistoryMonthSnapshot?
|
|
2128
|
-
@State private var historySelectedDay: Date?
|
|
2129
|
-
@State private var historyDayDetailMode: ProviderHistoryDayDetailMode = .summary
|
|
2130
|
-
@State private var historyIsLoading = false
|
|
2131
|
-
@State private var historyError: String?
|
|
2132
|
-
|
|
2133
|
-
var body: some View {
|
|
2134
|
-
let insightLines = ProviderInsightsComposer.lines(for: self.provider, store: self.store)
|
|
2135
|
-
let topModelLines = self.topModelLines
|
|
2136
|
-
let topProjectLines = self.topProjectLines
|
|
2137
|
-
|
|
2138
|
-
ScrollView {
|
|
2139
|
-
VStack(alignment: .leading, spacing: ProviderListMetrics.sidebarSectionSpacing) {
|
|
2140
|
-
ProviderSidebarSectionCard {
|
|
2141
|
-
VStack(alignment: .leading, spacing: RunicSpacing.md) {
|
|
2142
|
-
HStack(alignment: .top, spacing: RunicSpacing.sm) {
|
|
2143
|
-
ProviderListBrandIcon(provider: self.provider)
|
|
2144
|
-
VStack(alignment: .leading, spacing: RunicSpacing.xxs) {
|
|
2145
|
-
Text(self.store.metadata(for: self.provider).displayName)
|
|
2146
|
-
.font(RunicFont.title2.weight(.semibold))
|
|
2147
|
-
Text(self.subtitle)
|
|
2148
|
-
.font(RunicFont.caption)
|
|
2149
|
-
.foregroundStyle(.secondary)
|
|
2150
|
-
}
|
|
2151
|
-
Spacer()
|
|
2152
|
-
Toggle("Enabled", isOn: self.$isEnabled)
|
|
2153
|
-
.toggleStyle(.switch)
|
|
2154
|
-
.labelsHidden()
|
|
2155
|
-
}
|
|
2156
|
-
Divider()
|
|
2157
|
-
|
|
2158
|
-
Picker("View", selection: self.$selectedSubview) {
|
|
2159
|
-
ForEach(ProviderDetailSubview.allCases) { view in
|
|
2160
|
-
Text(view.rawValue).tag(view)
|
|
2161
|
-
}
|
|
2162
|
-
}
|
|
2163
|
-
.pickerStyle(.segmented)
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
if self.selectedSubview == .overview {
|
|
2168
|
-
ProviderSidebarSectionCard {
|
|
2169
|
-
VStack(alignment: .leading, spacing: RunicSpacing.xs) {
|
|
2170
|
-
VStack(alignment: .leading, spacing: RunicSpacing.xxs) {
|
|
2171
|
-
ProviderSidebarSectionHeader(title: "Overview")
|
|
2172
|
-
ProviderSidebarKeyValueRow(label: "Source", value: self.sourceLabel, helpText: nil)
|
|
2173
|
-
ProviderSidebarKeyValueRow(label: "Updated", value: self.statusLabel, helpText: nil)
|
|
2174
|
-
if let runtimeMetrics = self.runtimeMetrics {
|
|
2175
|
-
ProviderSidebarKeyValueRow(
|
|
2176
|
-
label: "Runtime",
|
|
2177
|
-
value: runtimeMetrics.lineText,
|
|
2178
|
-
helpText: runtimeMetrics.hoverText)
|
|
2179
|
-
}
|
|
2180
|
-
self.statusBadge
|
|
2181
|
-
}
|
|
2182
|
-
|
|
2183
|
-
if !self.quickMetrics.isEmpty {
|
|
2184
|
-
LazyVGrid(
|
|
2185
|
-
columns: [
|
|
2186
|
-
GridItem(.flexible(minimum: 120), spacing: RunicSpacing.xs),
|
|
2187
|
-
GridItem(.flexible(minimum: 120), spacing: RunicSpacing.xs),
|
|
2188
|
-
],
|
|
2189
|
-
alignment: .leading,
|
|
2190
|
-
spacing: RunicSpacing.xs)
|
|
2191
|
-
{
|
|
2192
|
-
ForEach(self.quickMetrics) { metric in
|
|
2193
|
-
ProviderSidebarMetricChip(
|
|
2194
|
-
title: metric.title,
|
|
2195
|
-
value: metric.value,
|
|
2196
|
-
helpText: metric.helpText)
|
|
2197
|
-
}
|
|
2198
|
-
}
|
|
2199
|
-
}
|
|
2200
|
-
|
|
2201
|
-
HStack(spacing: RunicSpacing.xs) {
|
|
2202
|
-
Button("Copy diagnostics") {
|
|
2203
|
-
self.copyDiagnostics()
|
|
2204
|
-
}
|
|
2205
|
-
.buttonStyle(.bordered)
|
|
2206
|
-
.controlSize(.small)
|
|
2207
|
-
.help("Copy fetch path, reliability, anomaly, and budget/forecast details.")
|
|
2208
|
-
|
|
2209
|
-
if let diagnosticsCopyStatus {
|
|
2210
|
-
Text(diagnosticsCopyStatus)
|
|
2211
|
-
.font(RunicFont.caption2)
|
|
2212
|
-
.foregroundStyle(.tertiary)
|
|
2213
|
-
}
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
2216
|
-
}
|
|
2217
|
-
|
|
2218
|
-
if !insightLines.isEmpty {
|
|
2219
|
-
ProviderSidebarSectionCard {
|
|
2220
|
-
VStack(alignment: .leading, spacing: RunicSpacing.xxs) {
|
|
2221
|
-
ProviderSidebarSectionHeader(title: "Insights")
|
|
2222
|
-
ProviderInsightsView(lines: insightLines)
|
|
2223
|
-
}
|
|
2224
|
-
}
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
if !topModelLines.isEmpty || !topProjectLines.isEmpty {
|
|
2228
|
-
ProviderSidebarSectionCard {
|
|
2229
|
-
VStack(alignment: .leading, spacing: RunicSpacing.xxs) {
|
|
2230
|
-
ProviderSidebarSectionHeader(title: "Activity leaders")
|
|
2231
|
-
HStack(alignment: .top, spacing: RunicSpacing.md) {
|
|
2232
|
-
if !topModelLines.isEmpty {
|
|
2233
|
-
VStack(alignment: .leading, spacing: RunicSpacing.xxs) {
|
|
2234
|
-
Text(self.modelSectionTitle)
|
|
2235
|
-
.font(RunicFont.caption2.weight(.semibold))
|
|
2236
|
-
.foregroundStyle(.secondary)
|
|
2237
|
-
ForEach(Array(topModelLines.enumerated()), id: \.offset) { index, line in
|
|
2238
|
-
Text("\(index + 1). \(line)")
|
|
2239
|
-
.font(RunicFont.caption)
|
|
2240
|
-
.foregroundStyle(.secondary)
|
|
2241
|
-
.textSelection(.enabled)
|
|
2242
|
-
}
|
|
2243
|
-
}
|
|
2244
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
2245
|
-
}
|
|
2246
|
-
|
|
2247
|
-
if !topProjectLines.isEmpty {
|
|
2248
|
-
VStack(alignment: .leading, spacing: RunicSpacing.xxs) {
|
|
2249
|
-
Text("Projects")
|
|
2250
|
-
.font(RunicFont.caption2.weight(.semibold))
|
|
2251
|
-
.foregroundStyle(.secondary)
|
|
2252
|
-
ForEach(Array(topProjectLines.enumerated()), id: \.offset) { index, line in
|
|
2253
|
-
Text("\(index + 1). \(line)")
|
|
2254
|
-
.font(RunicFont.caption)
|
|
2255
|
-
.foregroundStyle(.secondary)
|
|
2256
|
-
.textSelection(.enabled)
|
|
2257
|
-
}
|
|
2258
|
-
}
|
|
2259
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2264
|
-
}
|
|
2265
|
-
} else {
|
|
2266
|
-
self.historyContent
|
|
2267
|
-
}
|
|
2268
|
-
|
|
2269
|
-
if let errorDisplay, self.isEnabled {
|
|
2270
|
-
ProviderErrorView(
|
|
2271
|
-
title: "Last fetch failed:",
|
|
2272
|
-
display: errorDisplay,
|
|
2273
|
-
isExpanded: self.$isErrorExpanded,
|
|
2274
|
-
onCopy: { self.onCopyError(errorDisplay.full) })
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
if self.isEnabled, !self.settingsFields.isEmpty {
|
|
2278
|
-
ProviderSidebarSectionCard {
|
|
2279
|
-
VStack(alignment: .leading, spacing: RunicSpacing.sm) {
|
|
2280
|
-
ProviderSidebarSectionHeader(title: "Settings fields")
|
|
2281
|
-
ForEach(self.settingsFields) { field in
|
|
2282
|
-
VStack(alignment: .leading, spacing: RunicSpacing.xxs) {
|
|
2283
|
-
Text(field.title)
|
|
2284
|
-
.font(RunicFont.body.weight(.medium))
|
|
2285
|
-
Text(field.subtitle)
|
|
2286
|
-
.font(RunicFont.caption)
|
|
2287
|
-
.foregroundStyle(.secondary)
|
|
2288
|
-
switch field.kind {
|
|
2289
|
-
case .plain:
|
|
2290
|
-
TextField(field.placeholder ?? "", text: field.binding)
|
|
2291
|
-
.textFieldStyle(.roundedBorder)
|
|
2292
|
-
.font(RunicFont.callout)
|
|
2293
|
-
case .secure:
|
|
2294
|
-
SecureField(field.placeholder ?? "", text: field.binding)
|
|
2295
|
-
.textFieldStyle(.roundedBorder)
|
|
2296
|
-
.font(RunicFont.callout)
|
|
2297
|
-
}
|
|
2298
|
-
self.fieldActions(field.actions)
|
|
2299
|
-
}
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
}
|
|
2304
|
-
|
|
2305
|
-
if self.isEnabled, !self.settingsToggles.isEmpty {
|
|
2306
|
-
ProviderSidebarSectionCard {
|
|
2307
|
-
VStack(alignment: .leading, spacing: RunicSpacing.sm) {
|
|
2308
|
-
ProviderSidebarSectionHeader(title: "Settings toggles")
|
|
2309
|
-
ForEach(self.settingsToggles) { toggle in
|
|
2310
|
-
VStack(alignment: .leading, spacing: RunicSpacing.xxs) {
|
|
2311
|
-
Toggle(isOn: toggle.binding) {
|
|
2312
|
-
VStack(alignment: .leading, spacing: RunicSpacing.xxs) {
|
|
2313
|
-
Text(toggle.title)
|
|
2314
|
-
.font(RunicFont.body.weight(.medium))
|
|
2315
|
-
Text(toggle.subtitle)
|
|
2316
|
-
.font(RunicFont.caption)
|
|
2317
|
-
.foregroundStyle(.secondary)
|
|
2318
|
-
}
|
|
2319
|
-
}
|
|
2320
|
-
.toggleStyle(.checkbox)
|
|
2321
|
-
|
|
2322
|
-
if toggle.binding.wrappedValue {
|
|
2323
|
-
if let status = toggle.statusText?(), !status.isEmpty {
|
|
2324
|
-
Text(status)
|
|
2325
|
-
.font(RunicFont.caption)
|
|
2326
|
-
.foregroundStyle(.secondary)
|
|
2327
|
-
.padding(RunicSpacing.xs)
|
|
2328
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
2329
|
-
.background(
|
|
2330
|
-
RoundedRectangle(
|
|
2331
|
-
cornerRadius: ProviderListMetrics.sidebarMicroCardCornerRadius,
|
|
2332
|
-
style: .continuous)
|
|
2333
|
-
.fill(Color(
|
|
2334
|
-
nsColor: .controlBackgroundColor).opacity(
|
|
2335
|
-
ProviderListMetrics.sidebarMicroCardBackgroundOpacity)))
|
|
2336
|
-
}
|
|
2337
|
-
self.toggleActions(toggle.actions)
|
|
2338
|
-
}
|
|
2339
|
-
}
|
|
2340
|
-
}
|
|
2341
|
-
}
|
|
2342
|
-
}
|
|
2343
|
-
}
|
|
2344
|
-
}
|
|
2345
|
-
.padding(RunicSpacing.lg)
|
|
2346
|
-
}
|
|
2347
|
-
.frame(maxWidth: .infinity, minHeight: 0, alignment: .topLeading)
|
|
2348
|
-
.clipped()
|
|
2349
|
-
.task(id: self.historyTaskID) {
|
|
2350
|
-
guard self.selectedSubview == .history else { return }
|
|
2351
|
-
await self.loadHistoryMonth()
|
|
2352
|
-
}
|
|
2353
|
-
.onChange(of: self.provider) { _, _ in
|
|
2354
|
-
self.selectedSubview = .overview
|
|
2355
|
-
self.historyMetricMode = .tokens
|
|
2356
|
-
self.historyMonthStart = Self.monthStart(for: Date())
|
|
2357
|
-
self.historySnapshot = nil
|
|
2358
|
-
self.historySelectedDay = nil
|
|
2359
|
-
self.historyDayDetailMode = .summary
|
|
2360
|
-
self.historyError = nil
|
|
2361
|
-
self.historyIsLoading = false
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
|
-
private var statusBadge: some View {
|
|
2366
|
-
let (color, bg) = self.statusColors
|
|
2367
|
-
return Text(self.usageStatus.text)
|
|
2368
|
-
.font(RunicFont.caption2.weight(.medium))
|
|
2369
|
-
.padding(.horizontal, RunicSpacing.xs)
|
|
2370
|
-
.padding(.vertical, RunicSpacing.xxs)
|
|
2371
|
-
.background(bg)
|
|
2372
|
-
.foregroundStyle(color)
|
|
2373
|
-
.clipShape(.capsule)
|
|
2374
|
-
}
|
|
2375
|
-
|
|
2376
|
-
private var statusColors: (Color, Color) {
|
|
2377
|
-
switch self.usageStatus.style {
|
|
2378
|
-
case .success: (.green, Color.green.opacity(0.15))
|
|
2379
|
-
case .error: (.red, Color.red.opacity(0.15))
|
|
2380
|
-
case .neutral: (.secondary, Color(nsColor: .controlBackgroundColor))
|
|
2381
|
-
}
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
|
-
private var historyTaskID: String {
|
|
2385
|
-
let monthKey = Int(self.historyMonthStart.timeIntervalSince1970)
|
|
2386
|
-
return "\(self.provider.rawValue)-\(self.selectedSubview.rawValue)-\(monthKey)"
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2389
|
-
private var historyContent: some View {
|
|
2390
|
-
ProviderSidebarSectionCard {
|
|
2391
|
-
VStack(alignment: .leading, spacing: RunicSpacing.sm) {
|
|
2392
|
-
HStack(alignment: .center, spacing: RunicSpacing.xs) {
|
|
2393
|
-
ProviderHistoryNavigationButton(
|
|
2394
|
-
systemName: "chevron.left",
|
|
2395
|
-
enabled: true,
|
|
2396
|
-
help: "Previous month")
|
|
2397
|
-
{
|
|
2398
|
-
self.shiftHistoryMonth(by: -1)
|
|
2399
|
-
}
|
|
2400
|
-
|
|
2401
|
-
Text(self.historyMonthTitle)
|
|
2402
|
-
.font(RunicFont.caption.weight(.semibold))
|
|
2403
|
-
.foregroundStyle(.secondary)
|
|
2404
|
-
.frame(minWidth: 130, alignment: .leading)
|
|
2405
|
-
|
|
2406
|
-
ProviderHistoryNavigationButton(
|
|
2407
|
-
systemName: "chevron.right",
|
|
2408
|
-
enabled: self.canShiftHistoryForward,
|
|
2409
|
-
help: "Next month")
|
|
2410
|
-
{
|
|
2411
|
-
self.shiftHistoryMonth(by: 1)
|
|
2412
|
-
}
|
|
2413
|
-
|
|
2414
|
-
Spacer()
|
|
2415
|
-
|
|
2416
|
-
Picker("Metric", selection: self.$historyMetricMode) {
|
|
2417
|
-
ForEach(ProviderHistoryMetricMode.allCases) { mode in
|
|
2418
|
-
Text(mode.rawValue).tag(mode)
|
|
2419
|
-
}
|
|
2420
|
-
}
|
|
2421
|
-
.pickerStyle(.segmented)
|
|
2422
|
-
.frame(width: 220)
|
|
2423
|
-
}
|
|
2424
|
-
|
|
2425
|
-
if self.historyIsLoading, self.historySnapshot == nil {
|
|
2426
|
-
HStack(spacing: RunicSpacing.xs) {
|
|
2427
|
-
ProgressView()
|
|
2428
|
-
.controlSize(.small)
|
|
2429
|
-
Text("Loading history…")
|
|
2430
|
-
.font(RunicFont.caption)
|
|
2431
|
-
.foregroundStyle(.secondary)
|
|
2432
|
-
}
|
|
2433
|
-
.padding(.vertical, RunicSpacing.xs)
|
|
2434
|
-
} else if let snapshot = self.historySnapshot {
|
|
2435
|
-
if !snapshot.isSupported {
|
|
2436
|
-
Text(snapshot.note ?? "History is not available for this provider yet.")
|
|
2437
|
-
.font(RunicFont.caption)
|
|
2438
|
-
.foregroundStyle(.secondary)
|
|
2439
|
-
.padding(.vertical, RunicSpacing.xs)
|
|
2440
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
2441
|
-
.background(
|
|
2442
|
-
RoundedRectangle(
|
|
2443
|
-
cornerRadius: ProviderListMetrics.sidebarMicroCardCornerRadius,
|
|
2444
|
-
style: .continuous)
|
|
2445
|
-
.fill(Color(nsColor: .controlBackgroundColor)
|
|
2446
|
-
.opacity(ProviderListMetrics.sidebarCardBackgroundOpacity)))
|
|
2447
|
-
} else {
|
|
2448
|
-
if let note = snapshot.note, !note.isEmpty {
|
|
2449
|
-
Text(note)
|
|
2450
|
-
.font(RunicFont.caption2)
|
|
2451
|
-
.foregroundStyle(.tertiary)
|
|
2452
|
-
}
|
|
2453
|
-
Text(snapshot.days
|
|
2454
|
-
.count == 1 ? "1 active day in \(self.historyMonthTitle)." :
|
|
2455
|
-
"\(snapshot.days.count) active days in \(self.historyMonthTitle).")
|
|
2456
|
-
.font(RunicFont.caption2)
|
|
2457
|
-
.foregroundStyle(.secondary)
|
|
2458
|
-
.padding(.bottom, RunicSpacing.xxs)
|
|
2459
|
-
|
|
2460
|
-
self.historyCalendarGrid
|
|
2461
|
-
self.historyDayDetailCard
|
|
2462
|
-
}
|
|
2463
|
-
} else {
|
|
2464
|
-
Text("History is empty for this period.")
|
|
2465
|
-
.font(RunicFont.caption)
|
|
2466
|
-
.foregroundStyle(.secondary)
|
|
2467
|
-
.padding(.vertical, RunicSpacing.xs)
|
|
2468
|
-
}
|
|
2469
|
-
|
|
2470
|
-
if let historyError = self.historyError {
|
|
2471
|
-
VStack(alignment: .leading, spacing: RunicSpacing.xxs) {
|
|
2472
|
-
Text("History load failed")
|
|
2473
|
-
.font(RunicFont.caption.weight(.semibold))
|
|
2474
|
-
.foregroundStyle(.red)
|
|
2475
|
-
Text(historyError)
|
|
2476
|
-
.font(RunicFont.caption)
|
|
2477
|
-
.foregroundStyle(.secondary)
|
|
2478
|
-
.textSelection(.enabled)
|
|
2479
|
-
Button("Retry") {
|
|
2480
|
-
Task { await self.loadHistoryMonth(forceRefresh: true) }
|
|
2481
|
-
}
|
|
2482
|
-
.buttonStyle(.bordered)
|
|
2483
|
-
.controlSize(.small)
|
|
2484
|
-
}
|
|
2485
|
-
.padding(RunicSpacing.sm)
|
|
2486
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
2487
|
-
.background(
|
|
2488
|
-
RoundedRectangle(
|
|
2489
|
-
cornerRadius: ProviderListMetrics.sidebarMicroCardCornerRadius,
|
|
2490
|
-
style: .continuous)
|
|
2491
|
-
.fill(Color.red.opacity(0.06)))
|
|
2492
|
-
.overlay(
|
|
2493
|
-
RoundedRectangle(
|
|
2494
|
-
cornerRadius: ProviderListMetrics.sidebarMicroCardCornerRadius,
|
|
2495
|
-
style: .continuous)
|
|
2496
|
-
.strokeBorder(Color.red.opacity(0.28), lineWidth: 1))
|
|
2497
|
-
}
|
|
2498
|
-
|
|
2499
|
-
Text(
|
|
2500
|
-
"Local aggregated history only. Prompts, cookies, API keys, and raw payloads are never shown here.")
|
|
2501
|
-
.font(RunicFont.caption2)
|
|
2502
|
-
.foregroundStyle(.tertiary)
|
|
2503
|
-
}
|
|
2504
|
-
}
|
|
2505
|
-
}
|
|
2506
|
-
|
|
2507
|
-
private var historyCalendarGrid: some View {
|
|
2508
|
-
VStack(alignment: .leading, spacing: RunicSpacing.xs) {
|
|
2509
|
-
let weekdays = self.weekdaySymbols
|
|
2510
|
-
LazyVGrid(
|
|
2511
|
-
columns: Array(repeating: GridItem(.flexible(), spacing: RunicSpacing.xxs), count: 7),
|
|
2512
|
-
spacing: RunicSpacing.xxs)
|
|
2513
|
-
{
|
|
2514
|
-
ForEach(weekdays, id: \.self) { symbol in
|
|
2515
|
-
Text(symbol)
|
|
2516
|
-
.font(RunicFont.caption2.weight(.semibold))
|
|
2517
|
-
.foregroundStyle(.tertiary)
|
|
2518
|
-
.frame(maxWidth: .infinity)
|
|
2519
|
-
}
|
|
2520
|
-
}
|
|
2521
|
-
|
|
2522
|
-
LazyVGrid(
|
|
2523
|
-
columns: Array(repeating: GridItem(.flexible(), spacing: RunicSpacing.xxs), count: 7),
|
|
2524
|
-
spacing: RunicSpacing.xxs)
|
|
2525
|
-
{
|
|
2526
|
-
ForEach(self.calendarDaysForMonth, id: \.self) { day in
|
|
2527
|
-
let inMonth = self.historyCalendar.isDate(
|
|
2528
|
-
day,
|
|
2529
|
-
equalTo: self.historyMonthStart,
|
|
2530
|
-
toGranularity: .month)
|
|
2531
|
-
let normalizedDay = self.historyCalendar.startOfDay(for: day)
|
|
2532
|
-
let summary = self.historySummaryByDay[normalizedDay]
|
|
2533
|
-
let dayNumber = self.historyCalendar.component(.day, from: day)
|
|
2534
|
-
let isSelected = self.historySelectedDay
|
|
2535
|
-
.map { self.historyCalendar.isDate($0, inSameDayAs: day) } ?? false
|
|
2536
|
-
ProviderHistoryCalendarDayCell(
|
|
2537
|
-
dayNumber: dayNumber,
|
|
2538
|
-
isInMonth: inMonth,
|
|
2539
|
-
isSelected: isSelected,
|
|
2540
|
-
hasActivity: summary != nil,
|
|
2541
|
-
intensity: self.historyIntensity(for: summary),
|
|
2542
|
-
action: { self.historySelectedDay = normalizedDay })
|
|
2543
|
-
.help(self.historyDayHelp(for: day, summary: summary))
|
|
2544
|
-
}
|
|
2545
|
-
}
|
|
2546
|
-
}
|
|
2547
|
-
}
|
|
2548
|
-
|
|
2549
|
-
private var historyDayDetailCard: some View {
|
|
2550
|
-
ProviderSidebarSectionCard {
|
|
2551
|
-
VStack(alignment: .leading, spacing: RunicSpacing.sm) {
|
|
2552
|
-
HStack(alignment: .firstTextBaseline, spacing: RunicSpacing.xs) {
|
|
2553
|
-
Text("Day details")
|
|
2554
|
-
.font(RunicFont.caption.weight(.semibold))
|
|
2555
|
-
.foregroundStyle(.tertiary)
|
|
2556
|
-
.textCase(.uppercase)
|
|
2557
|
-
.tracking(0.25)
|
|
2558
|
-
Spacer()
|
|
2559
|
-
if self.selectedHistoryDaySummary != nil {
|
|
2560
|
-
Picker("History detail", selection: self.$historyDayDetailMode) {
|
|
2561
|
-
ForEach(ProviderHistoryDayDetailMode.allCases) { mode in
|
|
2562
|
-
Text(mode.rawValue).tag(mode)
|
|
2563
|
-
}
|
|
2564
|
-
}
|
|
2565
|
-
.pickerStyle(.segmented)
|
|
2566
|
-
.controlSize(.mini)
|
|
2567
|
-
.frame(maxWidth: 240)
|
|
2568
|
-
}
|
|
2569
|
-
}
|
|
2570
|
-
|
|
2571
|
-
if let selected = self.selectedHistoryDaySummary {
|
|
2572
|
-
Text(selected.dayStart.formatted(.dateTime.weekday(.wide).month(.abbreviated).day().year()))
|
|
2573
|
-
.font(RunicFont.caption.weight(.semibold))
|
|
2574
|
-
.foregroundStyle(.secondary)
|
|
2575
|
-
|
|
2576
|
-
if self.historyDayDetailMode == .summary {
|
|
2577
|
-
LazyVGrid(
|
|
2578
|
-
columns: [
|
|
2579
|
-
GridItem(.flexible(minimum: 90), spacing: RunicSpacing.xs),
|
|
2580
|
-
GridItem(.flexible(minimum: 90), spacing: RunicSpacing.xs),
|
|
2581
|
-
GridItem(.flexible(minimum: 90), spacing: RunicSpacing.xs),
|
|
2582
|
-
],
|
|
2583
|
-
alignment: .leading,
|
|
2584
|
-
spacing: RunicSpacing.xs)
|
|
2585
|
-
{
|
|
2586
|
-
ProviderSidebarMetricChip(
|
|
2587
|
-
title: "Requests",
|
|
2588
|
-
value: self.decimalString(selected.requestCount),
|
|
2589
|
-
helpText: "Count of requests recorded in local ledger logs.")
|
|
2590
|
-
ProviderSidebarMetricChip(
|
|
2591
|
-
title: "Tokens",
|
|
2592
|
-
value: UsageFormatter.tokenSummaryString(selected.totals),
|
|
2593
|
-
helpText: "Input, output, and cache token composition.")
|
|
2594
|
-
if let spend = selected.totals.costUSD {
|
|
2595
|
-
ProviderSidebarMetricChip(
|
|
2596
|
-
title: "Spend",
|
|
2597
|
-
value: UsageFormatter.usdString(spend),
|
|
2598
|
-
helpText: "Estimated day spend from ledger pricing.")
|
|
2599
|
-
}
|
|
2600
|
-
}
|
|
2601
|
-
|
|
2602
|
-
if self.hasModelBreakdown, let topModel = selected.topModel {
|
|
2603
|
-
Text(
|
|
2604
|
-
"Top model: \(self.usageLine(title: UsageFormatter.modelDisplayName(topModel.model), totals: topModel.totals, requests: topModel.entryCount, model: topModel.model))")
|
|
2605
|
-
.font(RunicFont.caption)
|
|
2606
|
-
.foregroundStyle(.secondary)
|
|
2607
|
-
.textSelection(.enabled)
|
|
2608
|
-
}
|
|
2609
|
-
|
|
2610
|
-
if self.hasProjectAttribution, let topProject = selected.topProject {
|
|
2611
|
-
let project = self.projectDisplay(topProject)
|
|
2612
|
-
Text("Top project: \(project.title)")
|
|
2613
|
-
.font(RunicFont.caption)
|
|
2614
|
-
.foregroundStyle(.secondary)
|
|
2615
|
-
.textSelection(.enabled)
|
|
2616
|
-
.help(project.helpText ?? "")
|
|
2617
|
-
}
|
|
2618
|
-
}
|
|
2619
|
-
|
|
2620
|
-
if self.historyDayDetailMode == .models {
|
|
2621
|
-
if self.hasModelBreakdown, !selected.modelSummaries.isEmpty {
|
|
2622
|
-
Text("Models used")
|
|
2623
|
-
.font(RunicFont.caption2.weight(.medium))
|
|
2624
|
-
.foregroundStyle(.secondary)
|
|
2625
|
-
ForEach(
|
|
2626
|
-
Array(selected.modelSummaries.prefix(12).enumerated()),
|
|
2627
|
-
id: \.offset)
|
|
2628
|
-
{ _, summary in
|
|
2629
|
-
Text("• \(self.historyModelLine(summary))")
|
|
2630
|
-
.font(RunicFont.caption2)
|
|
2631
|
-
.foregroundStyle(.tertiary)
|
|
2632
|
-
.textSelection(.enabled)
|
|
2633
|
-
.help(self.historyModelLine(summary))
|
|
2634
|
-
}
|
|
2635
|
-
} else if self.hasModelBreakdown, !selected.modelsUsed.isEmpty {
|
|
2636
|
-
Text("Models used: \(self.renderedModelsList(selected.modelsUsed).joined(separator: ", "))")
|
|
2637
|
-
.font(RunicFont.caption2)
|
|
2638
|
-
.foregroundStyle(.tertiary)
|
|
2639
|
-
.textSelection(.enabled)
|
|
2640
|
-
} else {
|
|
2641
|
-
Text(self
|
|
2642
|
-
.hasModelBreakdown ? "No models recorded for this day." :
|
|
2643
|
-
"Model attribution is not available for this provider.")
|
|
2644
|
-
.font(RunicFont.caption)
|
|
2645
|
-
.foregroundStyle(.tertiary)
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2648
|
-
|
|
2649
|
-
if self.historyDayDetailMode == .projects {
|
|
2650
|
-
if self.hasProjectAttribution, !selected.projectSummaries.isEmpty {
|
|
2651
|
-
Text("Top projects")
|
|
2652
|
-
.font(RunicFont.caption2.weight(.medium))
|
|
2653
|
-
.foregroundStyle(.secondary)
|
|
2654
|
-
ForEach(
|
|
2655
|
-
Array(selected.projectSummaries.prefix(12).enumerated()),
|
|
2656
|
-
id: \.offset)
|
|
2657
|
-
{ _, summary in
|
|
2658
|
-
let project = self.projectDisplay(summary)
|
|
2659
|
-
Text("• \(self.historyProjectLine(summary))")
|
|
2660
|
-
.font(RunicFont.caption2)
|
|
2661
|
-
.foregroundStyle(.tertiary)
|
|
2662
|
-
.textSelection(.enabled)
|
|
2663
|
-
.help(project.helpText ?? "")
|
|
2664
|
-
}
|
|
2665
|
-
} else {
|
|
2666
|
-
Text(self
|
|
2667
|
-
.hasProjectAttribution ? "No projects recorded for this day." :
|
|
2668
|
-
"Project attribution is not available for this provider.")
|
|
2669
|
-
.font(RunicFont.caption)
|
|
2670
|
-
.foregroundStyle(.tertiary)
|
|
2671
|
-
}
|
|
2672
|
-
}
|
|
2673
|
-
} else {
|
|
2674
|
-
Text(self.historySelectedDay?
|
|
2675
|
-
.formatted(.dateTime.weekday(.wide).month(.abbreviated).day().year()) ?? "No day selected")
|
|
2676
|
-
.font(RunicFont.caption.weight(.semibold))
|
|
2677
|
-
.foregroundStyle(.secondary)
|
|
2678
|
-
Text("No recorded activity for this day.")
|
|
2679
|
-
.font(RunicFont.caption)
|
|
2680
|
-
.foregroundStyle(.tertiary)
|
|
2681
|
-
}
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
|
-
}
|
|
2685
|
-
|
|
2686
|
-
private var historyMonthTitle: String {
|
|
2687
|
-
self.historyMonthStart.formatted(.dateTime.month(.wide).year())
|
|
2688
|
-
}
|
|
2689
|
-
|
|
2690
|
-
private var canShiftHistoryForward: Bool {
|
|
2691
|
-
self.historyMonthStart < Self.monthStart(for: Date())
|
|
2692
|
-
}
|
|
2693
|
-
|
|
2694
|
-
private var historyCalendar: Calendar {
|
|
2695
|
-
var calendar = Calendar(identifier: .gregorian)
|
|
2696
|
-
calendar.timeZone = TimeZone.current
|
|
2697
|
-
return calendar
|
|
2698
|
-
}
|
|
2699
|
-
|
|
2700
|
-
private var weekdaySymbols: [String] {
|
|
2701
|
-
let symbols = self.historyCalendar.veryShortStandaloneWeekdaySymbols
|
|
2702
|
-
guard !symbols.isEmpty else { return [] }
|
|
2703
|
-
let start = max(0, min(symbols.count - 1, self.historyCalendar.firstWeekday - 1))
|
|
2704
|
-
return Array(symbols[start...]) + Array(symbols[..<start])
|
|
2705
|
-
}
|
|
2706
|
-
|
|
2707
|
-
private var calendarDaysForMonth: [Date] {
|
|
2708
|
-
guard let monthInterval = self.historyCalendar.dateInterval(of: .month, for: self.historyMonthStart),
|
|
2709
|
-
let firstWeek = self.historyCalendar.dateInterval(of: .weekOfMonth, for: monthInterval.start),
|
|
2710
|
-
let lastDayOfMonth = self.historyCalendar.date(byAdding: .day, value: -1, to: monthInterval.end),
|
|
2711
|
-
let lastWeek = self.historyCalendar.dateInterval(of: .weekOfMonth, for: lastDayOfMonth)
|
|
2712
|
-
else {
|
|
2713
|
-
return []
|
|
2714
|
-
}
|
|
2715
|
-
|
|
2716
|
-
var days: [Date] = []
|
|
2717
|
-
var cursor = firstWeek.start
|
|
2718
|
-
while cursor < lastWeek.end {
|
|
2719
|
-
days.append(cursor)
|
|
2720
|
-
guard let next = self.historyCalendar.date(byAdding: .day, value: 1, to: cursor) else { break }
|
|
2721
|
-
cursor = next
|
|
2722
|
-
}
|
|
2723
|
-
return days
|
|
2724
|
-
}
|
|
2725
|
-
|
|
2726
|
-
private var historySummaryByDay: [Date: ProviderHistoryDaySnapshot] {
|
|
2727
|
-
guard let snapshot = self.historySnapshot else { return [:] }
|
|
2728
|
-
var map: [Date: ProviderHistoryDaySnapshot] = [:]
|
|
2729
|
-
for day in snapshot.days {
|
|
2730
|
-
map[self.historyCalendar.startOfDay(for: day.dayStart)] = day
|
|
2731
|
-
}
|
|
2732
|
-
return map
|
|
2733
|
-
}
|
|
2734
|
-
|
|
2735
|
-
private var selectedHistoryDaySummary: ProviderHistoryDaySnapshot? {
|
|
2736
|
-
guard let snapshot = self.historySnapshot else { return nil }
|
|
2737
|
-
guard let selectedDay = self.historySelectedDay else { return snapshot.days.last }
|
|
2738
|
-
return snapshot.days.first { self.historyCalendar.isDate($0.dayStart, inSameDayAs: selectedDay) }
|
|
2739
|
-
}
|
|
2740
|
-
|
|
2741
|
-
private var historyMaxMetricValue: Double {
|
|
2742
|
-
guard let snapshot = self.historySnapshot, !snapshot.days.isEmpty else { return 0 }
|
|
2743
|
-
return snapshot.days.reduce(0) { max($0, self.historyMetricValue(for: $1)) }
|
|
2744
|
-
}
|
|
2745
|
-
|
|
2746
|
-
private func historyMetricValue(for day: ProviderHistoryDaySnapshot) -> Double {
|
|
2747
|
-
switch self.historyMetricMode {
|
|
2748
|
-
case .tokens:
|
|
2749
|
-
Double(day.totals.totalTokens)
|
|
2750
|
-
case .cost:
|
|
2751
|
-
max(0, day.totals.costUSD ?? 0)
|
|
2752
|
-
case .requests:
|
|
2753
|
-
Double(day.requestCount)
|
|
2754
|
-
}
|
|
2755
|
-
}
|
|
2756
|
-
|
|
2757
|
-
private func historyIntensity(for day: ProviderHistoryDaySnapshot?) -> Double {
|
|
2758
|
-
guard let day else { return 0 }
|
|
2759
|
-
let maxValue = self.historyMaxMetricValue
|
|
2760
|
-
guard maxValue > 0 else { return 0.15 }
|
|
2761
|
-
return min(1, max(0.15, self.historyMetricValue(for: day) / maxValue))
|
|
2762
|
-
}
|
|
2763
|
-
|
|
2764
|
-
private func historyDayHelp(for day: Date, summary: ProviderHistoryDaySnapshot?) -> String {
|
|
2765
|
-
var lines: [String] = [day.formatted(.dateTime.weekday(.wide).month(.abbreviated).day().year())]
|
|
2766
|
-
guard let summary else {
|
|
2767
|
-
lines.append("No recorded activity")
|
|
2768
|
-
return lines.joined(separator: "\n")
|
|
2769
|
-
}
|
|
2770
|
-
lines.append("Requests: \(self.decimalString(summary.requestCount))")
|
|
2771
|
-
lines.append("Tokens: \(UsageFormatter.tokenSummaryString(summary.totals))")
|
|
2772
|
-
if let spend = summary.totals.costUSD {
|
|
2773
|
-
lines.append("Spend: \(UsageFormatter.usdString(spend))")
|
|
2774
|
-
}
|
|
2775
|
-
if self.hasModelBreakdown, let topModel = summary.topModel {
|
|
2776
|
-
var modelLine = "Top model: \(UsageFormatter.modelDisplayName(topModel.model))"
|
|
2777
|
-
if let context = UsageFormatter.modelContextLabel(for: topModel.model) {
|
|
2778
|
-
modelLine += " · \(context)"
|
|
2779
|
-
}
|
|
2780
|
-
lines.append(modelLine)
|
|
2781
|
-
}
|
|
2782
|
-
if self.hasModelBreakdown, !summary.modelSummaries.isEmpty {
|
|
2783
|
-
let modelCount = min(3, summary.modelSummaries.count)
|
|
2784
|
-
for summary in summary.modelSummaries.prefix(modelCount) {
|
|
2785
|
-
var line = "Model: \(UsageFormatter.modelDisplayName(summary.model)) · \(UsageFormatter.tokenCountString(summary.totals.totalTokens)) tok"
|
|
2786
|
-
if let context = UsageFormatter.modelContextLabel(for: summary.model) {
|
|
2787
|
-
line += " · \(context)"
|
|
2788
|
-
}
|
|
2789
|
-
lines.append(line)
|
|
2790
|
-
}
|
|
2791
|
-
}
|
|
2792
|
-
if self.hasProjectAttribution, let topProject = summary.topProject {
|
|
2793
|
-
var projectLine = "Top project: \(self.projectDisplay(topProject).title)"
|
|
2794
|
-
if let cost = topProject.totals.costUSD {
|
|
2795
|
-
projectLine += " · \(UsageFormatter.usdString(cost))"
|
|
2796
|
-
}
|
|
2797
|
-
lines.append(projectLine)
|
|
2798
|
-
}
|
|
2799
|
-
return lines.joined(separator: "\n")
|
|
2800
|
-
}
|
|
2801
|
-
|
|
2802
|
-
private func renderedModelsList(_ modelsUsed: [String]) -> [String] {
|
|
2803
|
-
var seen: Set<String> = []
|
|
2804
|
-
var rendered: [String] = []
|
|
2805
|
-
for model in modelsUsed {
|
|
2806
|
-
let trimmed = model.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
2807
|
-
guard !trimmed.isEmpty, seen.insert(trimmed).inserted else { continue }
|
|
2808
|
-
var text = UsageFormatter.modelDisplayName(trimmed)
|
|
2809
|
-
if let context = UsageFormatter.modelContextLabel(for: trimmed) {
|
|
2810
|
-
text += " \(context)"
|
|
2811
|
-
}
|
|
2812
|
-
rendered.append(text)
|
|
2813
|
-
}
|
|
2814
|
-
return rendered
|
|
2815
|
-
}
|
|
2816
|
-
|
|
2817
|
-
private func historyModelLine(_ summary: UsageLedgerModelSummary) -> String {
|
|
2818
|
-
self.usageLine(
|
|
2819
|
-
title: UsageFormatter.modelDisplayName(summary.model),
|
|
2820
|
-
totals: summary.totals,
|
|
2821
|
-
requests: summary.entryCount,
|
|
2822
|
-
model: summary.model)
|
|
2823
|
-
}
|
|
2824
|
-
|
|
2825
|
-
private func historyProjectLine(_ summary: UsageLedgerProjectSummary) -> String {
|
|
2826
|
-
let project = self.projectDisplay(summary)
|
|
2827
|
-
return self.usageLine(
|
|
2828
|
-
title: project.title,
|
|
2829
|
-
totals: summary.totals,
|
|
2830
|
-
requests: summary.entryCount)
|
|
2831
|
-
}
|
|
2832
|
-
|
|
2833
|
-
private func shiftHistoryMonth(by delta: Int) {
|
|
2834
|
-
guard delta != 0 else { return }
|
|
2835
|
-
guard let shifted = self.historyCalendar.date(byAdding: .month, value: delta, to: self.historyMonthStart) else {
|
|
2836
|
-
return
|
|
2837
|
-
}
|
|
2838
|
-
let candidate = Self.monthStart(for: shifted)
|
|
2839
|
-
if delta > 0, candidate > Self.monthStart(for: Date()) {
|
|
2840
|
-
return
|
|
2841
|
-
}
|
|
2842
|
-
self.historyMonthStart = candidate
|
|
2843
|
-
self.historySnapshot = nil
|
|
2844
|
-
self.historySelectedDay = nil
|
|
2845
|
-
self.historyError = nil
|
|
2846
|
-
}
|
|
2847
|
-
|
|
2848
|
-
private static func monthStart(for date: Date) -> Date {
|
|
2849
|
-
var calendar = Calendar(identifier: .gregorian)
|
|
2850
|
-
calendar.timeZone = TimeZone.current
|
|
2851
|
-
let components = calendar.dateComponents([.year, .month], from: date)
|
|
2852
|
-
return calendar.date(from: components) ?? calendar.startOfDay(for: date)
|
|
2853
|
-
}
|
|
2854
|
-
|
|
2855
|
-
private func loadHistoryMonth(forceRefresh: Bool = false) async {
|
|
2856
|
-
guard !self.historyIsLoading else { return }
|
|
2857
|
-
self.historyIsLoading = true
|
|
2858
|
-
self.historyError = nil
|
|
2859
|
-
let snapshot = await self.store.providerHistoryMonth(
|
|
2860
|
-
provider: self.provider,
|
|
2861
|
-
monthStart: self.historyMonthStart,
|
|
2862
|
-
forceRefresh: forceRefresh)
|
|
2863
|
-
self.historySnapshot = snapshot
|
|
2864
|
-
self.historyError = snapshot.error
|
|
2865
|
-
self.historyIsLoading = false
|
|
2866
|
-
self.selectDefaultHistoryDay(from: snapshot)
|
|
2867
|
-
}
|
|
2868
|
-
|
|
2869
|
-
private func selectDefaultHistoryDay(from snapshot: ProviderHistoryMonthSnapshot) {
|
|
2870
|
-
guard !snapshot.days.isEmpty else {
|
|
2871
|
-
self.historySelectedDay = self.historyCalendar.startOfDay(for: self.historyMonthStart)
|
|
2872
|
-
return
|
|
2873
|
-
}
|
|
2874
|
-
if let selected = self.historySelectedDay,
|
|
2875
|
-
snapshot.days.contains(where: { self.historyCalendar.isDate($0.dayStart, inSameDayAs: selected) })
|
|
2876
|
-
{
|
|
2877
|
-
self.historySelectedDay = self.historyCalendar.startOfDay(for: selected)
|
|
2878
|
-
return
|
|
2879
|
-
}
|
|
2880
|
-
self.historySelectedDay = snapshot.days.map(\.dayStart).max()
|
|
2881
|
-
}
|
|
2882
|
-
|
|
2883
|
-
private struct ProjectDisplay {
|
|
2884
|
-
let title: String
|
|
2885
|
-
let helpText: String?
|
|
2886
|
-
}
|
|
2887
|
-
|
|
2888
|
-
private func projectDisplay(_ summary: UsageLedgerProjectSummary) -> ProjectDisplay {
|
|
2889
|
-
let displayName = summary.displayProjectName
|
|
2890
|
-
let source = summary.projectNameSource ?? .unknown
|
|
2891
|
-
let confidence = summary.projectNameConfidence ?? .none
|
|
2892
|
-
let shouldAnnotateSource = source != .projectName && source != .budgetOverride
|
|
2893
|
-
let shouldAnnotateConfidence = confidence != .high
|
|
2894
|
-
let isUnknown = displayName == "Unknown project"
|
|
2895
|
-
|
|
2896
|
-
var details: [String] = []
|
|
2897
|
-
if shouldAnnotateSource {
|
|
2898
|
-
details.append("source: \(self.projectSourceLabel(source))")
|
|
2899
|
-
}
|
|
2900
|
-
if shouldAnnotateConfidence {
|
|
2901
|
-
details.append("confidence: \(self.projectConfidenceLabel(confidence))")
|
|
2902
|
-
}
|
|
2903
|
-
if isUnknown, let fingerprint = self.projectIDFingerprint(summary.projectID) {
|
|
2904
|
-
details.append("id: \(fingerprint)")
|
|
2905
|
-
}
|
|
2906
|
-
if let provenance = self.trimmed(summary.projectNameProvenance) {
|
|
2907
|
-
details.append("via: \(provenance)")
|
|
2908
|
-
}
|
|
2909
|
-
return ProjectDisplay(
|
|
2910
|
-
title: displayName,
|
|
2911
|
-
helpText: details.isEmpty ? nil : details.joined(separator: "\n"))
|
|
2912
|
-
}
|
|
2913
|
-
|
|
2914
|
-
private func projectSourceLabel(_ source: UsageLedgerProjectNameSource) -> String {
|
|
2915
|
-
switch source {
|
|
2916
|
-
case .projectName: "project name"
|
|
2917
|
-
case .projectID: "project id"
|
|
2918
|
-
case .inferredFromPath: "path-derived"
|
|
2919
|
-
case .inferredFromName: "name-derived"
|
|
2920
|
-
case .budgetOverride: "budget override"
|
|
2921
|
-
case .unknown: "unknown"
|
|
2922
|
-
}
|
|
2923
|
-
}
|
|
2924
|
-
|
|
2925
|
-
private func projectConfidenceLabel(_ confidence: UsageLedgerProjectNameConfidence) -> String {
|
|
2926
|
-
switch confidence {
|
|
2927
|
-
case .high: "high"
|
|
2928
|
-
case .medium: "medium"
|
|
2929
|
-
case .low: "low"
|
|
2930
|
-
case .none: "none"
|
|
2931
|
-
}
|
|
2932
|
-
}
|
|
2933
|
-
|
|
2934
|
-
private func projectIDFingerprint(_ projectID: String?) -> String? {
|
|
2935
|
-
guard let projectID = projectID?.trimmingCharacters(in: .whitespacesAndNewlines), !projectID.isEmpty else {
|
|
2936
|
-
return nil
|
|
2937
|
-
}
|
|
2938
|
-
var hash: UInt64 = 0xCBF2_9CE4_8422_2325
|
|
2939
|
-
for byte in projectID.lowercased().utf8 {
|
|
2940
|
-
hash ^= UInt64(byte)
|
|
2941
|
-
hash = hash &* 0x100_0000_01B3
|
|
2942
|
-
}
|
|
2943
|
-
return String(format: "%08llx", hash)
|
|
2944
|
-
}
|
|
2945
|
-
|
|
2946
|
-
@ViewBuilder
|
|
2947
|
-
private func fieldActions(_ actions: [ProviderSettingsActionDescriptor]) -> some View {
|
|
2948
|
-
let visible = actions.filter { $0.isVisible?() ?? true }
|
|
2949
|
-
if !visible.isEmpty {
|
|
2950
|
-
HStack(spacing: RunicSpacing.xs) {
|
|
2951
|
-
ForEach(visible) { action in
|
|
2952
|
-
Button(action.title) {
|
|
2953
|
-
Task { @MainActor in await action.perform() }
|
|
2954
|
-
}
|
|
2955
|
-
.applyProviderSettingsButtonStyle(action.style)
|
|
2956
|
-
.controlSize(.small)
|
|
2957
|
-
}
|
|
2958
|
-
}
|
|
2959
|
-
}
|
|
2960
|
-
}
|
|
2961
|
-
|
|
2962
|
-
@ViewBuilder
|
|
2963
|
-
private func toggleActions(_ actions: [ProviderSettingsActionDescriptor]) -> some View {
|
|
2964
|
-
let visible = actions.filter { $0.isVisible?() ?? true }
|
|
2965
|
-
if !visible.isEmpty {
|
|
2966
|
-
HStack(spacing: RunicSpacing.xs) {
|
|
2967
|
-
ForEach(visible) { action in
|
|
2968
|
-
Button(action.title) {
|
|
2969
|
-
Task { @MainActor in await action.perform() }
|
|
2970
|
-
}
|
|
2971
|
-
.applyProviderSettingsButtonStyle(action.style)
|
|
2972
|
-
.controlSize(.small)
|
|
2973
|
-
}
|
|
2974
|
-
}
|
|
2975
|
-
}
|
|
2976
|
-
}
|
|
2977
|
-
|
|
2978
|
-
private struct QuickMetricItem: Identifiable {
|
|
2979
|
-
let id: String
|
|
2980
|
-
let title: String
|
|
2981
|
-
let value: String
|
|
2982
|
-
let helpText: String?
|
|
2983
|
-
}
|
|
2984
|
-
|
|
2985
|
-
private var quickMetrics: [QuickMetricItem] {
|
|
2986
|
-
var items: [QuickMetricItem] = []
|
|
2987
|
-
let snapshot = self.store.snapshot(for: self.provider)
|
|
2988
|
-
let tokenSnapshot = self.store.tokenSnapshot(for: self.provider)
|
|
2989
|
-
let hasModelBreakdown = self.hasModelBreakdown
|
|
2990
|
-
let hasProjectAttribution = self.hasProjectAttribution
|
|
2991
|
-
|
|
2992
|
-
if let today = self
|
|
2993
|
-
.tokenWindowValue(tokens: tokenSnapshot?.sessionTokens, cost: tokenSnapshot?.sessionCostUSD)
|
|
2994
|
-
{
|
|
2995
|
-
items.append(QuickMetricItem(
|
|
2996
|
-
id: "today",
|
|
2997
|
-
title: "Today",
|
|
2998
|
-
value: today,
|
|
2999
|
-
helpText: "Session cost and tokens."))
|
|
3000
|
-
}
|
|
3001
|
-
if let last30 = self.tokenWindowValue(
|
|
3002
|
-
tokens: tokenSnapshot?.last30DaysTokens,
|
|
3003
|
-
cost: tokenSnapshot?.last30DaysCostUSD)
|
|
3004
|
-
{
|
|
3005
|
-
items.append(QuickMetricItem(
|
|
3006
|
-
id: "30d",
|
|
3007
|
-
title: "30d",
|
|
3008
|
-
value: last30,
|
|
3009
|
-
helpText: "Last 30 days cost and tokens."))
|
|
3010
|
-
}
|
|
3011
|
-
if let spend = self.providerSpendValue(snapshot?.providerCost) {
|
|
3012
|
-
items.append(QuickMetricItem(
|
|
3013
|
-
id: "spend",
|
|
3014
|
-
title: "Spend",
|
|
3015
|
-
value: spend,
|
|
3016
|
-
helpText: "Provider billing usage."))
|
|
3017
|
-
}
|
|
3018
|
-
if hasModelBreakdown, let topModel = self.store.ledgerTopModel(for: self.provider) {
|
|
3019
|
-
let modelName = UsageFormatter.modelDisplayName(topModel.model)
|
|
3020
|
-
var value = self.modelLineValue(
|
|
3021
|
-
title: modelName,
|
|
3022
|
-
totals: topModel.totals,
|
|
3023
|
-
requests: topModel.entryCount)
|
|
3024
|
-
if let context = UsageFormatter.modelContextLabel(for: topModel.model) {
|
|
3025
|
-
value += " · \(context)"
|
|
3026
|
-
}
|
|
3027
|
-
items.append(QuickMetricItem(
|
|
3028
|
-
id: "top-model",
|
|
3029
|
-
title: "Top model",
|
|
3030
|
-
value: value,
|
|
3031
|
-
helpText: "Highest token usage model in the active insights window."))
|
|
3032
|
-
} else if !hasModelBreakdown, let windowModel = self.topWindowModel {
|
|
3033
|
-
let modelName = UsageFormatter.modelDisplayName(windowModel.label)
|
|
3034
|
-
let used = Int(windowModel.window.usedPercent.rounded())
|
|
3035
|
-
items.append(QuickMetricItem(
|
|
3036
|
-
id: "top-model-window",
|
|
3037
|
-
title: hasModelBreakdown ? "Top model" : "Top window",
|
|
3038
|
-
value: "\(modelName) · \(used)% used",
|
|
3039
|
-
helpText: hasModelBreakdown ?
|
|
3040
|
-
"Most constrained model/category from live quota windows."
|
|
3041
|
-
: "Top quota window from live fetch response."))
|
|
3042
|
-
}
|
|
3043
|
-
if hasProjectAttribution, let topProject = self.store.ledgerTopProject(for: self.provider) {
|
|
3044
|
-
let project = self.projectDisplay(topProject)
|
|
3045
|
-
let value = self.topProjectSummaryValue(topProject)
|
|
3046
|
-
items.append(QuickMetricItem(
|
|
3047
|
-
id: "top-project",
|
|
3048
|
-
title: "Top project",
|
|
3049
|
-
value: "\(project.title) · \(value)",
|
|
3050
|
-
helpText: "Highest token usage project in the active insights window."))
|
|
3051
|
-
}
|
|
3052
|
-
if let coverage = ProviderInsightsComposer.coverageSummaryLabel(for: self.provider, store: self.store) {
|
|
3053
|
-
let value = coverage.replacingOccurrences(of: "usage: ", with: "")
|
|
3054
|
-
items.append(QuickMetricItem(
|
|
3055
|
-
id: "coverage",
|
|
3056
|
-
title: "Data",
|
|
3057
|
-
value: value,
|
|
3058
|
-
helpText: "Provider coverage for model-level usage, token metrics, and project attribution."))
|
|
3059
|
-
}
|
|
3060
|
-
return items
|
|
3061
|
-
}
|
|
3062
|
-
|
|
3063
|
-
private func tokenWindowValue(tokens: Int?, cost: Double?) -> String? {
|
|
3064
|
-
var parts: [String] = []
|
|
3065
|
-
if let cost, cost.isFinite, cost >= 0 {
|
|
3066
|
-
parts.append(UsageFormatter.usdString(cost))
|
|
3067
|
-
}
|
|
3068
|
-
if let tokens, tokens >= 0 {
|
|
3069
|
-
parts.append("\(UsageFormatter.tokenCountString(tokens)) tok")
|
|
3070
|
-
}
|
|
3071
|
-
guard !parts.isEmpty else { return nil }
|
|
3072
|
-
return parts.joined(separator: " · ")
|
|
3073
|
-
}
|
|
3074
|
-
|
|
3075
|
-
private func providerSpendValue(_ providerCost: ProviderCostSnapshot?) -> String? {
|
|
3076
|
-
guard let providerCost else { return nil }
|
|
3077
|
-
let used = UsageFormatter.currencyString(providerCost.used, currencyCode: providerCost.currencyCode)
|
|
3078
|
-
if providerCost.limit > 0 {
|
|
3079
|
-
let limitText = UsageFormatter.currencyString(providerCost.limit, currencyCode: providerCost.currencyCode)
|
|
3080
|
-
return "\(used) / \(limitText)"
|
|
3081
|
-
}
|
|
3082
|
-
return used
|
|
3083
|
-
}
|
|
3084
|
-
|
|
3085
|
-
private var topModelLines: [String] {
|
|
3086
|
-
let ranked = self.store.ledgerModelBreakdown(for: self.provider).sorted { lhs, rhs in
|
|
3087
|
-
if lhs.totals.totalTokens == rhs.totals.totalTokens {
|
|
3088
|
-
return UsageFormatter.modelDisplayName(lhs.model) < UsageFormatter.modelDisplayName(rhs.model)
|
|
3089
|
-
}
|
|
3090
|
-
return lhs.totals.totalTokens > rhs.totals.totalTokens
|
|
3091
|
-
}
|
|
3092
|
-
if !ranked.isEmpty {
|
|
3093
|
-
guard self.hasModelBreakdown else {
|
|
3094
|
-
return []
|
|
3095
|
-
}
|
|
3096
|
-
return ranked.prefix(3).map { summary in
|
|
3097
|
-
let name = UsageFormatter.modelDisplayName(summary.model)
|
|
3098
|
-
return self.usageLine(
|
|
3099
|
-
title: name,
|
|
3100
|
-
totals: summary.totals,
|
|
3101
|
-
requests: summary.entryCount,
|
|
3102
|
-
model: summary.model)
|
|
3103
|
-
}
|
|
3104
|
-
}
|
|
3105
|
-
guard !self.hasModelBreakdown else {
|
|
3106
|
-
return []
|
|
3107
|
-
}
|
|
3108
|
-
return self.windowModelLines
|
|
3109
|
-
}
|
|
3110
|
-
|
|
3111
|
-
private var modelSectionTitle: String {
|
|
3112
|
-
self.hasModelBreakdown ? "Models" : "Quota windows"
|
|
3113
|
-
}
|
|
3114
|
-
|
|
3115
|
-
private var effectiveUsageCoverage: ProviderUsageCoverage {
|
|
3116
|
-
ProviderInsightsComposer.effectiveCoverage(for: self.provider, store: self.store)
|
|
3117
|
-
}
|
|
3118
|
-
|
|
3119
|
-
private var hasModelBreakdown: Bool {
|
|
3120
|
-
self.effectiveUsageCoverage.supportsModelBreakdown
|
|
3121
|
-
}
|
|
3122
|
-
|
|
3123
|
-
private var hasProjectAttribution: Bool {
|
|
3124
|
-
self.effectiveUsageCoverage.supportsProjectAttribution
|
|
3125
|
-
}
|
|
3126
|
-
|
|
3127
|
-
private var topProjectLines: [String] {
|
|
3128
|
-
guard self.hasProjectAttribution else {
|
|
3129
|
-
return []
|
|
3130
|
-
}
|
|
3131
|
-
let ranked = self.store.ledgerProjectBreakdown(for: self.provider).sorted { lhs, rhs in
|
|
3132
|
-
if lhs.totals.totalTokens == rhs.totals.totalTokens {
|
|
3133
|
-
return lhs.displayProjectName < rhs.displayProjectName
|
|
3134
|
-
}
|
|
3135
|
-
return lhs.totals.totalTokens > rhs.totals.totalTokens
|
|
3136
|
-
}
|
|
3137
|
-
return ranked.prefix(3).map { summary in
|
|
3138
|
-
let project = summary.displayProjectName
|
|
3139
|
-
return self.usageLine(title: project, totals: summary.totals, requests: summary.entryCount)
|
|
3140
|
-
}
|
|
3141
|
-
}
|
|
3142
|
-
|
|
3143
|
-
private func usageLine(
|
|
3144
|
-
title: String,
|
|
3145
|
-
totals: UsageLedgerTotals,
|
|
3146
|
-
requests: Int,
|
|
3147
|
-
model: String? = nil) -> String
|
|
3148
|
-
{
|
|
3149
|
-
let tokens = UsageFormatter.tokenSummaryString(totals)
|
|
3150
|
-
var parts = ["\(title)", "\(tokens)", "\(requests) req"]
|
|
3151
|
-
if let cost = totals.costUSD {
|
|
3152
|
-
parts.append(UsageFormatter.usdString(cost))
|
|
3153
|
-
if let per1K = UsageFormatter.usdPer1KTokensString(costUSD: cost, tokenCount: totals.totalTokens) {
|
|
3154
|
-
parts.append("~\(per1K)")
|
|
3155
|
-
}
|
|
3156
|
-
if let perReq = UsageFormatter.usdPerRequestString(costUSD: cost, requestCount: requests) {
|
|
3157
|
-
parts.append("~\(perReq)")
|
|
3158
|
-
}
|
|
3159
|
-
}
|
|
3160
|
-
if let model, let context = UsageFormatter.modelContextLabel(for: model) {
|
|
3161
|
-
parts.append(context)
|
|
3162
|
-
}
|
|
3163
|
-
return parts.joined(separator: " · ")
|
|
3164
|
-
}
|
|
3165
|
-
|
|
3166
|
-
private func topProjectSummaryValue(_ summary: UsageLedgerProjectSummary) -> String {
|
|
3167
|
-
var parts: [String] = []
|
|
3168
|
-
parts.append(UsageFormatter.tokenSummaryString(summary.totals))
|
|
3169
|
-
parts.append("\(summary.entryCount) req")
|
|
3170
|
-
if let cost = summary.totals.costUSD {
|
|
3171
|
-
parts.append(UsageFormatter.usdString(cost))
|
|
3172
|
-
if let perReq = UsageFormatter.usdPerRequestString(costUSD: cost, requestCount: summary.entryCount) {
|
|
3173
|
-
parts.append("~\(perReq)")
|
|
3174
|
-
}
|
|
3175
|
-
}
|
|
3176
|
-
return parts.joined(separator: " · ")
|
|
3177
|
-
}
|
|
3178
|
-
|
|
3179
|
-
private func modelLineValue(title: String, totals: UsageLedgerTotals, requests: Int) -> String {
|
|
3180
|
-
var parts: [String] = [title]
|
|
3181
|
-
parts.append(UsageFormatter.tokenSummaryString(totals))
|
|
3182
|
-
parts.append("\(requests) req")
|
|
3183
|
-
if let cost = totals.costUSD {
|
|
3184
|
-
parts.append(UsageFormatter.usdString(cost))
|
|
3185
|
-
if let per1K = UsageFormatter.usdPer1KTokensString(costUSD: cost, tokenCount: totals.totalTokens) {
|
|
3186
|
-
parts.append("~\(per1K)")
|
|
3187
|
-
}
|
|
3188
|
-
if let perReq = UsageFormatter.usdPerRequestString(costUSD: cost, requestCount: requests) {
|
|
3189
|
-
parts.append("~\(perReq)")
|
|
3190
|
-
}
|
|
3191
|
-
}
|
|
3192
|
-
return parts.joined(separator: " · ")
|
|
3193
|
-
}
|
|
3194
|
-
|
|
3195
|
-
private var windowModelLines: [String] {
|
|
3196
|
-
self.labeledQuotaWindows(from: self.store.snapshot(for: self.provider)).prefix(3).map { item in
|
|
3197
|
-
let modelName = UsageFormatter.modelDisplayName(item.label)
|
|
3198
|
-
let used = Int(item.window.usedPercent.rounded())
|
|
3199
|
-
let remaining = Int(item.window.remainingPercent.rounded())
|
|
3200
|
-
var parts = [modelName, "\(used)% used", "\(remaining)% left"]
|
|
3201
|
-
if let resetsAt = item.window.resetsAt {
|
|
3202
|
-
parts.append("reset \(UsageFormatter.resetCountdownDescription(from: resetsAt))")
|
|
3203
|
-
} else if let resetDescription = item.window.resetDescription?
|
|
3204
|
-
.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
3205
|
-
!resetDescription.isEmpty
|
|
3206
|
-
{
|
|
3207
|
-
parts.append(resetDescription)
|
|
3208
|
-
}
|
|
3209
|
-
return parts.joined(separator: " · ")
|
|
3210
|
-
}
|
|
3211
|
-
}
|
|
3212
|
-
|
|
3213
|
-
private var topWindowModel: (label: String, window: RateWindow)? {
|
|
3214
|
-
self.labeledQuotaWindows(from: self.store.snapshot(for: self.provider)).first
|
|
3215
|
-
}
|
|
3216
|
-
|
|
3217
|
-
private func labeledQuotaWindows(from snapshot: UsageSnapshot?) -> [(label: String, window: RateWindow)] {
|
|
3218
|
-
guard let snapshot else { return [] }
|
|
3219
|
-
let windows = [snapshot.primary, snapshot.secondary, snapshot.tertiary].compactMap(\.self)
|
|
3220
|
-
var seen: Set<String> = []
|
|
3221
|
-
var labeled: [(label: String, window: RateWindow)] = []
|
|
3222
|
-
|
|
3223
|
-
for window in windows {
|
|
3224
|
-
guard let label = window.label?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
3225
|
-
!label.isEmpty
|
|
3226
|
-
else {
|
|
3227
|
-
continue
|
|
3228
|
-
}
|
|
3229
|
-
let normalized = label.lowercased()
|
|
3230
|
-
guard seen.insert(normalized).inserted else { continue }
|
|
3231
|
-
labeled.append((label, window))
|
|
3232
|
-
}
|
|
3233
|
-
|
|
3234
|
-
return labeled.sorted { lhs, rhs in
|
|
3235
|
-
if lhs.window.usedPercent == rhs.window.usedPercent {
|
|
3236
|
-
return lhs.label.localizedCaseInsensitiveCompare(rhs.label) == .orderedAscending
|
|
3237
|
-
}
|
|
3238
|
-
return lhs.window.usedPercent > rhs.window.usedPercent
|
|
3239
|
-
}
|
|
3240
|
-
}
|
|
3241
|
-
|
|
3242
|
-
private func decimalString(_ value: Int) -> String {
|
|
3243
|
-
let formatter = NumberFormatter()
|
|
3244
|
-
formatter.numberStyle = .decimal
|
|
3245
|
-
formatter.usesGroupingSeparator = true
|
|
3246
|
-
formatter.locale = Locale(identifier: "en_US_POSIX")
|
|
3247
|
-
return formatter.string(from: NSNumber(value: value)) ?? "\(value)"
|
|
3248
|
-
}
|
|
3249
|
-
|
|
3250
|
-
private struct RuntimeMetricsData {
|
|
3251
|
-
let lineText: String
|
|
3252
|
-
let hoverText: String?
|
|
3253
|
-
}
|
|
3254
|
-
|
|
3255
|
-
private var runtimeMetrics: RuntimeMetricsData? {
|
|
3256
|
-
let attempts = self.store.fetchAttempts(for: self.provider)
|
|
3257
|
-
guard !attempts.isEmpty || self.store.snapshot(for: self.provider) != nil else {
|
|
3258
|
-
return nil
|
|
3259
|
-
}
|
|
3260
|
-
|
|
3261
|
-
var parts: [String] = []
|
|
3262
|
-
if let updatedAt = self.store.snapshot(for: self.provider)?.updatedAt {
|
|
3263
|
-
parts.append("success \(updatedAt.relativeDescription())")
|
|
3264
|
-
} else {
|
|
3265
|
-
parts.append("no success yet")
|
|
3266
|
-
}
|
|
3267
|
-
|
|
3268
|
-
var hoverText: String?
|
|
3269
|
-
if !attempts.isEmpty {
|
|
3270
|
-
let retryCount = self.retryCount(from: attempts)
|
|
3271
|
-
if retryCount > 0 {
|
|
3272
|
-
parts.append("retry \(retryCount)")
|
|
3273
|
-
}
|
|
3274
|
-
if let activeAttempt = self.activeAttempt(from: attempts) {
|
|
3275
|
-
parts.append(Self.fetchKindLabel(activeAttempt.kind))
|
|
3276
|
-
if let strategyID = self.trimmed(activeAttempt.strategyID) {
|
|
3277
|
-
hoverText = "Strategy: \(strategyID)"
|
|
3278
|
-
}
|
|
3279
|
-
}
|
|
3280
|
-
}
|
|
3281
|
-
|
|
3282
|
-
return RuntimeMetricsData(
|
|
3283
|
-
lineText: parts.joined(separator: " · "),
|
|
3284
|
-
hoverText: hoverText)
|
|
3285
|
-
}
|
|
3286
|
-
|
|
3287
|
-
private func retryCount(from attempts: [ProviderFetchAttempt]) -> Int {
|
|
3288
|
-
guard !attempts.isEmpty else { return 0 }
|
|
3289
|
-
if let successIndex = attempts
|
|
3290
|
-
.firstIndex(where: { $0.wasAvailable && self.trimmed($0.errorDescription) == nil })
|
|
3291
|
-
{
|
|
3292
|
-
return max(0, successIndex)
|
|
3293
|
-
}
|
|
3294
|
-
return max(0, attempts.count - 1)
|
|
3295
|
-
}
|
|
3296
|
-
|
|
3297
|
-
private func activeAttempt(from attempts: [ProviderFetchAttempt]) -> ProviderFetchAttempt? {
|
|
3298
|
-
guard !attempts.isEmpty else { return nil }
|
|
3299
|
-
return attempts.first(where: { $0.wasAvailable && self.trimmed($0.errorDescription) == nil }) ??
|
|
3300
|
-
attempts.last(where: { $0.wasAvailable }) ??
|
|
3301
|
-
attempts.last
|
|
3302
|
-
}
|
|
3303
|
-
|
|
3304
|
-
private func trimmed(_ value: String?) -> String? {
|
|
3305
|
-
guard let value else { return nil }
|
|
3306
|
-
let cleaned = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
3307
|
-
return cleaned.isEmpty ? nil : cleaned
|
|
3308
|
-
}
|
|
3309
|
-
|
|
3310
|
-
private static func fetchKindLabel(_ kind: ProviderFetchKind) -> String {
|
|
3311
|
-
switch kind {
|
|
3312
|
-
case .cli: "cli"
|
|
3313
|
-
case .web: "web"
|
|
3314
|
-
case .oauth: "oauth"
|
|
3315
|
-
case .apiToken: "api"
|
|
3316
|
-
case .api: "api"
|
|
3317
|
-
case .localProbe: "local"
|
|
3318
|
-
case .webDashboard: "web"
|
|
3319
|
-
}
|
|
3320
|
-
}
|
|
3321
|
-
|
|
3322
|
-
private func copyDiagnostics() {
|
|
3323
|
-
let pb = NSPasteboard.general
|
|
3324
|
-
pb.clearContents()
|
|
3325
|
-
pb.setString(self.diagnosticsReport, forType: .string)
|
|
3326
|
-
self.diagnosticsCopyStatus = "Copied"
|
|
3327
|
-
Task { @MainActor in
|
|
3328
|
-
try? await Task.sleep(nanoseconds: 1_500_000_000)
|
|
3329
|
-
self.diagnosticsCopyStatus = nil
|
|
3330
|
-
}
|
|
3331
|
-
}
|
|
3332
|
-
|
|
3333
|
-
private var diagnosticsReport: String {
|
|
3334
|
-
let metadata = self.store.metadata(for: self.provider)
|
|
3335
|
-
let attempts = self.store.fetchAttempts(for: self.provider)
|
|
3336
|
-
let snapshot = self.store.snapshot(for: self.provider)
|
|
3337
|
-
let forecast = self.store.ledgerSpendForecast(for: self.provider)
|
|
3338
|
-
let topProjectForecast = self.store.ledgerTopProjectSpendForecast(for: self.provider)
|
|
3339
|
-
let reliability = self.store.ledgerReliabilityScore(for: self.provider)
|
|
3340
|
-
let anomaly = self.store.ledgerAnomalySummary(for: self.provider)
|
|
3341
|
-
let iso = ISO8601DateFormatter()
|
|
3342
|
-
|
|
3343
|
-
var lines: [String] = []
|
|
3344
|
-
lines.append("# \(metadata.displayName) Diagnostics")
|
|
3345
|
-
lines.append("provider: \(self.provider.rawValue)")
|
|
3346
|
-
lines.append("generated_at: \(iso.string(from: Date()))")
|
|
3347
|
-
lines.append("enabled: \(self.isEnabled ? "true" : "false")")
|
|
3348
|
-
lines.append("source: \(self.sourceLabel)")
|
|
3349
|
-
lines.append("updated: \(self.statusLabel)")
|
|
3350
|
-
if let runtime = self.runtimeMetrics?.lineText {
|
|
3351
|
-
lines.append("runtime: \(runtime)")
|
|
3352
|
-
}
|
|
3353
|
-
|
|
3354
|
-
if let snapshot {
|
|
3355
|
-
lines.append("")
|
|
3356
|
-
lines.append("usage_snapshot:")
|
|
3357
|
-
lines.append("- updated_at: \(iso.string(from: snapshot.updatedAt))")
|
|
3358
|
-
lines.append("- primary_used_percent: \(Int(snapshot.primary.usedPercent.rounded()))")
|
|
3359
|
-
if let minutes = snapshot.primary.windowMinutes, minutes > 0 {
|
|
3360
|
-
lines.append("- primary_window_minutes: \(minutes)")
|
|
3361
|
-
}
|
|
3362
|
-
if let reset = snapshot.primary.resetsAt {
|
|
3363
|
-
lines.append("- primary_resets_at: \(iso.string(from: reset))")
|
|
3364
|
-
}
|
|
3365
|
-
if let cost = snapshot.providerCost {
|
|
3366
|
-
lines
|
|
3367
|
-
.append(
|
|
3368
|
-
"- provider_spend_used: \(UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode))")
|
|
3369
|
-
if cost.limit > 0 {
|
|
3370
|
-
lines
|
|
3371
|
-
.append(
|
|
3372
|
-
"- provider_spend_limit: \(UsageFormatter.currencyString(cost.limit, currencyCode: cost.currencyCode))")
|
|
3373
|
-
}
|
|
3374
|
-
if let period = self.trimmed(cost.period) {
|
|
3375
|
-
lines.append("- provider_spend_period: \(period)")
|
|
3376
|
-
}
|
|
3377
|
-
}
|
|
3378
|
-
}
|
|
3379
|
-
|
|
3380
|
-
lines.append("")
|
|
3381
|
-
lines.append("fetch_path:")
|
|
3382
|
-
if attempts.isEmpty {
|
|
3383
|
-
lines.append("- none")
|
|
3384
|
-
} else {
|
|
3385
|
-
for attempt in attempts {
|
|
3386
|
-
let state = if !attempt.wasAvailable {
|
|
3387
|
-
"unavailable"
|
|
3388
|
-
} else if let error = self.trimmed(attempt.errorDescription) {
|
|
3389
|
-
"failed: \(error)"
|
|
3390
|
-
} else {
|
|
3391
|
-
"ok"
|
|
3392
|
-
}
|
|
3393
|
-
lines.append("- \(attempt.strategyID) [\(Self.fetchKindLabel(attempt.kind))] \(state)")
|
|
3394
|
-
}
|
|
3395
|
-
}
|
|
3396
|
-
|
|
3397
|
-
if let forecast {
|
|
3398
|
-
lines.append("")
|
|
3399
|
-
lines.append("provider_forecast:")
|
|
3400
|
-
lines.append("- projected_30d: \(UsageFormatter.usdString(forecast.projected30DayCostUSD))")
|
|
3401
|
-
lines.append("- average_daily: \(UsageFormatter.usdString(forecast.averageDailyCostUSD))")
|
|
3402
|
-
if let p50 = forecast.projectedCostP50USD {
|
|
3403
|
-
lines.append("- p50: \(UsageFormatter.usdString(p50))")
|
|
3404
|
-
}
|
|
3405
|
-
if let p80 = forecast.projectedCostP80USD {
|
|
3406
|
-
lines.append("- p80: \(UsageFormatter.usdString(p80))")
|
|
3407
|
-
}
|
|
3408
|
-
if let p95 = forecast.projectedCostP95USD {
|
|
3409
|
-
lines.append("- p95: \(UsageFormatter.usdString(p95))")
|
|
3410
|
-
}
|
|
3411
|
-
if let limit = forecast.budgetLimitUSD, limit > 0 {
|
|
3412
|
-
lines.append("- budget_limit: \(UsageFormatter.usdString(limit))")
|
|
3413
|
-
lines.append("- budget_status: \(self.budgetStatusText(forecast))")
|
|
3414
|
-
}
|
|
3415
|
-
}
|
|
3416
|
-
|
|
3417
|
-
if let topProjectForecast {
|
|
3418
|
-
lines.append("")
|
|
3419
|
-
lines.append("top_project_forecast:")
|
|
3420
|
-
if let name = self.trimmed(topProjectForecast.projectName) {
|
|
3421
|
-
lines.append("- project: \(name)")
|
|
3422
|
-
}
|
|
3423
|
-
lines.append("- projected_30d: \(UsageFormatter.usdString(topProjectForecast.projected30DayCostUSD))")
|
|
3424
|
-
if let limit = topProjectForecast.budgetLimitUSD, limit > 0 {
|
|
3425
|
-
lines.append("- budget_limit: \(UsageFormatter.usdString(limit))")
|
|
3426
|
-
lines.append("- budget_status: \(self.budgetStatusText(topProjectForecast))")
|
|
3427
|
-
}
|
|
3428
|
-
}
|
|
3429
|
-
|
|
3430
|
-
if let reliability {
|
|
3431
|
-
lines.append("")
|
|
3432
|
-
lines.append("reliability:")
|
|
3433
|
-
lines.append("- score: \(reliability.score)/100")
|
|
3434
|
-
lines.append("- grade: \(reliability.grade)")
|
|
3435
|
-
lines.append("- summary: \(reliability.summary)")
|
|
3436
|
-
if let signal = self.trimmed(reliability.primarySignal) {
|
|
3437
|
-
lines.append("- signal: \(signal)")
|
|
3438
|
-
}
|
|
3439
|
-
}
|
|
3440
|
-
|
|
3441
|
-
if let anomaly {
|
|
3442
|
-
lines.append("")
|
|
3443
|
-
lines.append("anomaly:")
|
|
3444
|
-
if let spend = anomaly.spendAnomaly {
|
|
3445
|
-
lines.append("- spend: \(spend.severity.label) +\(Int((spend.percentIncrease * 100).rounded()))%")
|
|
3446
|
-
}
|
|
3447
|
-
if let token = anomaly.tokenAnomaly {
|
|
3448
|
-
lines.append("- tokens: \(token.severity.label) +\(Int((token.percentIncrease * 100).rounded()))%")
|
|
3449
|
-
}
|
|
3450
|
-
if let explanation = anomaly.explanation {
|
|
3451
|
-
lines.append("- headline: \(explanation.headline)")
|
|
3452
|
-
for detail in explanation.details {
|
|
3453
|
-
lines.append("- detail: \(detail)")
|
|
3454
|
-
}
|
|
3455
|
-
}
|
|
3456
|
-
}
|
|
3457
|
-
|
|
3458
|
-
return lines.joined(separator: "\n")
|
|
3459
|
-
}
|
|
3460
|
-
|
|
3461
|
-
private func budgetStatusText(_ forecast: UsageLedgerSpendForecast) -> String {
|
|
3462
|
-
if let eta = forecast.budgetETAInDays {
|
|
3463
|
-
return self.budgetBreachETAText(days: eta)
|
|
3464
|
-
}
|
|
3465
|
-
if forecast.budgetWillBreach {
|
|
3466
|
-
return "Breach risk"
|
|
3467
|
-
}
|
|
3468
|
-
return "On track"
|
|
3469
|
-
}
|
|
3470
|
-
|
|
3471
|
-
private func budgetBreachETAText(days: Double) -> String {
|
|
3472
|
-
guard days.isFinite else { return "Breach ETA unavailable" }
|
|
3473
|
-
if days <= 0 { return "Breach now" }
|
|
3474
|
-
let now = Date()
|
|
3475
|
-
let etaDate = now.addingTimeInterval(days * 24 * 60 * 60)
|
|
3476
|
-
let countdown = UsageFormatter.resetCountdownDescription(from: etaDate, now: now)
|
|
3477
|
-
if countdown == "now" { return "Breach now" }
|
|
3478
|
-
return "Breach \(countdown)"
|
|
3479
|
-
}
|
|
3480
|
-
}
|