@sriinnu/runic 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSE +1 -1
- package/README.md +47 -29
- package/RuniciOS/Core/RunicTypographyiOS.swift +24 -0
- package/RuniciOS/Views/ProviderDetailView.swift +9 -9
- package/RuniciOS/Views/ProviderListView.swift +10 -10
- package/RuniciOS/Widgets/LargeWidget.swift +14 -14
- package/RuniciOS/Widgets/LiveActivity.swift +19 -19
- package/RuniciOS/Widgets/LockScreenWidget.swift +15 -15
- package/RuniciOS/Widgets/MediumWidget.swift +10 -10
- package/RuniciOS/Widgets/RunicWidget.swift +5 -5
- package/RuniciOS/Widgets/SmallWidget.swift +9 -9
- package/Scripts/package_app.sh +2 -2
- package/Sources/Runic/About.swift +3 -3
- package/Sources/Runic/AppNotifications.swift +5 -3
- package/Sources/Runic/Controllers/StatusItemController+Actions.swift +17 -4
- package/Sources/Runic/Controllers/StatusItemController+Animation.swift +5 -4
- package/Sources/Runic/Controllers/StatusItemController+ChartSubmenus.swift +30 -16
- package/Sources/Runic/Controllers/StatusItemController+CustomProviders.swift +6 -3
- package/Sources/Runic/Controllers/StatusItemController+Menu.swift +47 -14
- package/Sources/Runic/Controllers/StatusItemController+MenuCards.swift +6 -4
- package/Sources/Runic/Controllers/StatusItemController+MenuHelpers.swift +40 -12
- package/Sources/Runic/Controllers/StatusItemController.swift +43 -43
- package/Sources/Runic/Core/BudgetNotificationManager.swift +2 -1
- package/Sources/Runic/Core/Login/ClaudeLoginRunner.swift +2 -2
- package/Sources/Runic/Core/Login/CodexLoginRunner.swift +1 -1
- package/Sources/Runic/Core/Login/CursorLoginRunner.swift +4 -4
- package/Sources/Runic/Core/Login/GeminiLoginRunner.swift +3 -6
- package/Sources/Runic/Core/Rendering/IconRenderer.swift +91 -85
- package/Sources/Runic/Core/Rendering/IconView.swift +3 -1
- package/Sources/Runic/Core/RunicColors.swift +6 -6
- package/Sources/Runic/Core/RunicSpacing.swift +7 -7
- package/Sources/Runic/Core/RunicTypography.swift +206 -0
- package/Sources/Runic/Core/Stores/AuggieTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/AzureOpenAITokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/CerebrasTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/CohereTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/CopilotTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/CustomProviderStore.swift +10 -10
- package/Sources/Runic/Core/Stores/DeepSeekTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/FireworksTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/GroqTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/KimiTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/MiniMaxCookieHeaderStore.swift +10 -36
- package/Sources/Runic/Core/Stores/MiniMaxGroupIDStore.swift +10 -36
- package/Sources/Runic/Core/Stores/MiniMaxTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/MistralTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/OpenRouterTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/PerplexityTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/ProviderToggleStore.swift +1 -1
- package/Sources/Runic/Core/Stores/QwenTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/SambaNovaTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/SettingsStore.swift +99 -52
- package/Sources/Runic/Core/Stores/TogetherTokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/UsageStore+Accessors.swift +19 -6
- package/Sources/Runic/Core/Stores/UsageStore+TokenCost.swift +1 -1
- package/Sources/Runic/Core/Stores/UsageStore+WidgetSnapshot.swift +1 -1
- package/Sources/Runic/Core/Stores/UsageStore.swift +234 -167
- package/Sources/Runic/Core/Stores/XAITokenStore.swift +10 -36
- package/Sources/Runic/Core/Stores/ZaiTokenStore.swift +10 -36
- package/Sources/Runic/Core/UsageExporter.swift +8 -4
- package/Sources/Runic/ProviderRegistry.swift +1 -1
- package/Sources/Runic/Providers/Antigravity/AntigravityProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Auggie/AuggieProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Azure/AzureProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Bedrock/BedrockProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Cerebras/CerebrasProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Codex/CodexProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Cohere/CohereProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Cursor/CursorProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/DeepSeek/DeepSeekProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Factory/FactoryProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Fireworks/FireworksProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Gemini/GeminiProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Groq/GroqProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Kimi/KimiProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/MiniMax/MiniMaxProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Mistral/MistralProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/OpenRouter/OpenRouterProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Perplexity/PerplexityProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/SambaNova/SambaNovaProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Shared/ProviderImplementation.swift +5 -2
- package/Sources/Runic/Providers/Shared/ProviderImplementationRegistry.swift +1 -1
- package/Sources/Runic/Providers/Shared/ProviderSettingsDescriptors.swift +1 -1
- package/Sources/Runic/Providers/Together/TogetherProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/XAI/XAIProviderImplementation.swift +1 -1
- package/Sources/Runic/Providers/Zai/ZaiProviderImplementation.swift +1 -1
- 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/provider-context-windows.json +20 -0
- package/Sources/Runic/RunicApp.swift +46 -36
- package/Sources/Runic/SessionQuotaNotifications.swift +2 -2
- package/Sources/Runic/UsagePaceText.swift +1 -1
- package/Sources/Runic/UsageProgressBar.swift +13 -9
- package/Sources/Runic/Utilities/LoadingPattern.swift +4 -2
- package/Sources/Runic/Utilities/PerformanceConstants.swift +28 -28
- package/Sources/Runic/Views/Menu/AlertsMenuView.swift +19 -21
- package/Sources/Runic/Views/Menu/CostComparativeChartMenuView.swift +19 -19
- package/Sources/Runic/Views/Menu/CostHistoryChartMenuView.swift +7 -7
- package/Sources/Runic/Views/Menu/CreditsHistoryChartMenuView.swift +5 -5
- package/Sources/Runic/Views/Menu/EfficiencyMetricsMenuView.swift +29 -30
- package/Sources/Runic/Views/Menu/HeroStatCardsMenuView.swift +20 -17
- package/Sources/Runic/Views/Menu/HourlyActivityChartMenuView.swift +14 -14
- package/Sources/Runic/Views/Menu/InlineUsageChartView.swift +15 -20
- package/Sources/Runic/Views/Menu/MenuCardEntranceModifier.swift +3 -4
- package/Sources/Runic/Views/Menu/MenuCardSidebarView.swift +5 -25
- package/Sources/Runic/Views/Menu/MenuCardSkeletonView.swift +1 -1
- package/Sources/Runic/Views/Menu/MenuCardView.swift +120 -131
- package/Sources/Runic/Views/Menu/MenuContent.swift +11 -8
- package/Sources/Runic/Views/Menu/MenuDescriptor.swift +4 -2
- package/Sources/Runic/Views/Menu/MenuHighlightStyle.swift +1 -8
- package/Sources/Runic/Views/Menu/ModelBreakdownMenuView.swift +20 -14
- package/Sources/Runic/Views/Menu/OverviewMenuView.swift +198 -58
- package/Sources/Runic/Views/Menu/ProjectBreakdownMenuView.swift +16 -11
- package/Sources/Runic/Views/Menu/ProjectBudgetMenuView.swift +27 -27
- package/Sources/Runic/Views/Menu/ProviderTabBarView.swift +4 -6
- package/Sources/Runic/Views/Menu/SubscriptionUtilizationChartMenuView.swift +24 -11
- package/Sources/Runic/Views/Menu/UsageBreakdownChartMenuView.swift +5 -5
- package/Sources/Runic/Views/Menu/UsageHeatmapMenuView.swift +7 -8
- package/Sources/Runic/Views/Menu/UsageTimelineChartMenuView.swift +28 -76
- package/Sources/Runic/Views/Menu/UsageWindowComparisonChartMenuView.swift +8 -7
- package/Sources/Runic/Views/Menu/WeeklyActivityChartMenuView.swift +7 -7
- package/Sources/Runic/Views/Performance/PerformanceDashboardView.swift +50 -47
- package/Sources/Runic/Views/Preferences/LiquidComponents.swift +15 -14
- package/Sources/Runic/Views/Preferences/PreferencesAboutPane.swift +7 -7
- package/Sources/Runic/Views/Preferences/PreferencesAdvancedPane.swift +27 -27
- package/Sources/Runic/Views/Preferences/PreferencesAlertsPane.swift +38 -42
- package/Sources/Runic/Views/Preferences/PreferencesAnalyticsPane.swift +39 -43
- package/Sources/Runic/Views/Preferences/PreferencesBudgetsPane.swift +21 -21
- package/Sources/Runic/Views/Preferences/PreferencesComponents.swift +7 -7
- package/Sources/Runic/Views/Preferences/PreferencesCustomProvidersPane.swift +49 -50
- package/Sources/Runic/Views/Preferences/PreferencesDebugPane.swift +10 -10
- package/Sources/Runic/Views/Preferences/PreferencesGeneralPane.swift +40 -21
- package/Sources/Runic/Views/Preferences/PreferencesHelpPane.swift +7 -7
- package/Sources/Runic/Views/Preferences/PreferencesIntegrationsPane.swift +108 -33
- package/Sources/Runic/Views/Preferences/PreferencesPerformancePane.swift +26 -23
- package/Sources/Runic/Views/Preferences/PreferencesProvidersPane.swift +271 -226
- package/Sources/Runic/Views/Preferences/PreferencesSelection.swift +6 -2
- package/Sources/Runic/Views/Preferences/PreferencesSyncPane.swift +7 -5
- package/Sources/Runic/Views/Preferences/PreferencesView.swift +38 -31
- package/Sources/Runic/Views/Rating/QualityRatingView.swift +11 -11
- package/Sources/Runic/Views/Team/TeamDashboardView.swift +29 -24
- package/Sources/Runic/Views/Team/TeamInviteSheet.swift +16 -16
- package/Sources/Runic/Views/Team/TeamManagementView.swift +50 -49
- package/Sources/Runic/Views/Team/TeamMemberRow.swift +8 -8
- package/Sources/RunicCLI/CLIEntry.swift +83 -42
- package/Sources/RunicCLI/CLIRenderer.swift +1 -1
- package/Sources/RunicCLI/Commands/EnhancedUsageCommand.swift +60 -60
- package/Sources/RunicCLI/Commands/ModelsCommand.swift +45 -44
- package/Sources/RunicCLI/Commands/ProjectsCommand.swift +83 -69
- package/Sources/RunicCLI/Commands/ResetCommand.swift +50 -49
- package/Sources/RunicCLI/Commands/UsageLedgerSourceFactory.swift +31 -32
- package/Sources/RunicClaudeWebProbe/main.swift +1 -1
- package/Sources/RunicCore/AlertRuleStore.swift +20 -20
- package/Sources/RunicCore/BrowserCookieImportOrder.swift +3 -3
- package/Sources/RunicCore/CostUsageModels.swift +1 -1
- package/Sources/RunicCore/CustomProviders/CustomProviderModels.swift +29 -30
- package/Sources/RunicCore/CustomProviders/GenericProviderFetcher.swift +80 -71
- package/Sources/RunicCore/CustomProviders/ProviderTemplates.swift +53 -109
- package/Sources/RunicCore/Host/PTY/TTYCommandRunner.swift +3 -3
- package/Sources/RunicCore/Integrations/GitHubIntegration.swift +3 -3
- package/Sources/RunicCore/Logging/OSLogLogHandler.swift +6 -6
- package/Sources/RunicCore/Models/EnhancedUsageModels.swift +47 -48
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardBrowserCookieImporter.swift +1 -1
- package/Sources/RunicCore/OpenAIWeb/OpenAIDashboardFetcher.swift +1 -1
- package/Sources/RunicCore/Performance/PerformanceModels.swift +4 -4
- package/Sources/RunicCore/Performance/PerformanceStorageImpl.swift +81 -64
- package/Sources/RunicCore/Policy/UsagePolicyEngine.swift +2 -2
- package/Sources/RunicCore/ProjectBudgetStore.swift +15 -12
- package/Sources/RunicCore/Providers/Antigravity/AntigravityProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/Antigravity/AntigravityStatusProbe.swift +2 -2
- package/Sources/RunicCore/Providers/Auggie/AuggieProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Auggie/AuggieUsageFetcher.swift +3 -3
- package/Sources/RunicCore/Providers/Azure/AzureProviderDescriptor.swift +8 -8
- package/Sources/RunicCore/Providers/Azure/AzureUsageFetcher.swift +5 -3
- package/Sources/RunicCore/Providers/Bedrock/BedrockProviderDescriptor.swift +6 -6
- package/Sources/RunicCore/Providers/Bedrock/BedrockUsageFetcher.swift +3 -7
- package/Sources/RunicCore/Providers/Cerebras/CerebrasProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Cerebras/CerebrasUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/Claude/ClaudeOAuth/ClaudeOAuthUsageFetcher.swift +3 -3
- package/Sources/RunicCore/Providers/Claude/ClaudeProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/Claude/ClaudeStatusProbe.swift +4 -4
- package/Sources/RunicCore/Providers/Claude/ClaudeUsageDataSource.swift +3 -1
- package/Sources/RunicCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift +1 -1
- package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthCredentials.swift +1 -0
- package/Sources/RunicCore/Providers/Codex/CodexOAuth/CodexOAuthUsageFetcher.swift +12 -1
- package/Sources/RunicCore/Providers/Codex/CodexProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/Codex/CodexUsageDataSource.swift +3 -1
- package/Sources/RunicCore/Providers/Codex/CodexWebDashboardStrategy.swift +5 -3
- package/Sources/RunicCore/Providers/Cohere/CohereProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Cohere/CohereUsageFetcher.swift +3 -3
- package/Sources/RunicCore/Providers/Copilot/CopilotGitHubCLITokenReader.swift +7 -4
- package/Sources/RunicCore/Providers/Copilot/CopilotProviderDescriptor.swift +4 -4
- package/Sources/RunicCore/Providers/Copilot/CopilotUsageFetcher.swift +5 -3
- package/Sources/RunicCore/Providers/Copilot/CopilotVSCodeTokenReader.swift +9 -10
- package/Sources/RunicCore/Providers/Cursor/CursorProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/Cursor/CursorStatusProbe.swift +1 -0
- package/Sources/RunicCore/Providers/DeepSeek/DeepSeekProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/DeepSeek/DeepSeekUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/Factory/FactoryLocalStorageImporter.swift +4 -4
- package/Sources/RunicCore/Providers/Factory/FactoryProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/Factory/FactoryStatusProbe.swift +3 -2
- package/Sources/RunicCore/Providers/Fireworks/FireworksProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Fireworks/FireworksUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/Gemini/GeminiProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/Gemini/GeminiStatusProbe.swift +6 -2
- package/Sources/RunicCore/Providers/Groq/GroqProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Groq/GroqUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/Kimi/KimiProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Kimi/KimiUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift +10 -5
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxUsageFetcher.swift +8 -11
- package/Sources/RunicCore/Providers/MiniMax/MiniMaxWebUsageFetcher.swift +10 -10
- package/Sources/RunicCore/Providers/Mistral/MistralProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Mistral/MistralUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/OpenRouter/OpenRouterUsageFetcher.swift +25 -16
- package/Sources/RunicCore/Providers/Perplexity/PerplexityProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Perplexity/PerplexityUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/ProviderFetchPlan.swift +2 -2
- package/Sources/RunicCore/Providers/ProviderTokenResolver.swift +2 -2
- package/Sources/RunicCore/Providers/Qwen/QwenProviderDescriptor.swift +5 -3
- package/Sources/RunicCore/Providers/Qwen/QwenUsageStats.swift +27 -16
- package/Sources/RunicCore/Providers/SambaNova/SambaNovaProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/SambaNova/SambaNovaUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/Together/TogetherProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/Together/TogetherUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/VertexAI/VertexAIProviderDescriptor.swift +5 -5
- package/Sources/RunicCore/Providers/VertexAI/VertexAISettingsReader.swift +1 -1
- package/Sources/RunicCore/Providers/VertexAI/VertexAIUsageFetcher.swift +3 -3
- package/Sources/RunicCore/Providers/XAI/XAIProviderDescriptor.swift +2 -2
- package/Sources/RunicCore/Providers/XAI/XAIUsageFetcher.swift +2 -2
- package/Sources/RunicCore/Providers/Zai/ZaiProviderDescriptor.swift +4 -2
- package/Sources/RunicCore/Providers/Zai/ZaiUsageStats.swift +51 -30
- package/Sources/RunicCore/Sync/BackgroundSyncManager.swift +76 -77
- package/Sources/RunicCore/Sync/SyncConflictResolver.swift +55 -58
- package/Sources/RunicCore/Sync/SyncProtocol.swift +21 -22
- package/Sources/RunicCore/Sync/SyncRecord.swift +46 -49
- package/Sources/RunicCore/Sync/SyncUsageExample.swift +44 -50
- package/Sources/RunicCore/Sync/TeamSyncRecords.swift +94 -101
- package/Sources/RunicCore/Sync/iCloudSyncEngine.swift +51 -46
- package/Sources/RunicCore/Team/TeamModels.swift +111 -110
- package/Sources/RunicCore/Team/TeamStore.swift +126 -109
- package/Sources/RunicCore/UsageFetcher.swift +2 -2
- package/Sources/RunicCore/UsageFormatter.swift +25 -25
- package/Sources/RunicCore/UsageLedger/ClaudeUsageLogSource.swift +141 -29
- package/Sources/RunicCore/UsageLedger/CodexUsageLogSource.swift +59 -5
- package/Sources/RunicCore/UsageLedger/LedgerCache.swift +161 -0
- package/Sources/RunicCore/UsageLedger/OTelGenAILedgerAdapter.swift +1 -1
- package/Sources/RunicCore/UsageLedger/UsageLedgerAggregator.swift +40 -25
- package/Sources/RunicCore/UsageLedger/UsageLedgerInsightsAdvisor.swift +1 -1
- package/Sources/RunicCore/UsageLedger/UsageLedgerProjectIdentity.swift +55 -16
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageCache.swift +6 -6
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageJsonl.swift +1 -1
- package/Sources/RunicCore/Vendored/CostUsage/CostUsagePricing.swift +2 -2
- package/Sources/RunicCore/Vendored/CostUsage/CostUsageScanner.swift +5 -5
- package/Tests/RunicTests/AppDelegateTests.swift +8 -3
- package/Tests/RunicTests/BrowserCookieOrderLabelTests.swift +2 -3
- package/Tests/RunicTests/CLIArgumentParsingTests.swift +3 -4
- package/Tests/RunicTests/CLICostTests.swift +6 -7
- package/Tests/RunicTests/CLIProviderSelectionTests.swift +2 -3
- package/Tests/RunicTests/CLISnapshotTests.swift +3 -4
- package/Tests/RunicTests/CLIWebFallbackTests.swift +3 -4
- package/Tests/RunicTests/ClaudeOAuthTests.swift +11 -12
- package/Tests/RunicTests/ClaudeResilienceTests.swift +3 -4
- package/Tests/RunicTests/ClaudeUsageTests.swift +20 -21
- package/Tests/RunicTests/CodexOAuthTests.swift +7 -8
- package/Tests/RunicTests/CodexUsageLogSourceTests.swift +1 -3
- package/Tests/RunicTests/CopilotTokenResolverTests.swift +5 -6
- package/Tests/RunicTests/CopilotVSCodeTokenReaderTests.swift +4 -5
- package/Tests/RunicTests/CostUsageDecodingTests.swift +15 -17
- package/Tests/RunicTests/CostUsagePricingTests.swift +5 -7
- package/Tests/RunicTests/CostUsageScannerTests.swift +10 -12
- package/Tests/RunicTests/CursorStatusProbeTests.swift +15 -16
- package/Tests/RunicTests/FactoryStatusProbeTests.swift +3 -5
- package/Tests/RunicTests/GeminiLoginAlertTests.swift +3 -4
- package/Tests/RunicTests/GeminiStatusProbeAPITests.swift +11 -11
- package/Tests/RunicTests/GeminiStatusProbePlanTests.swift +8 -8
- package/Tests/RunicTests/GeminiStatusProbeTests.swift +20 -21
- package/Tests/RunicTests/GoogleWorkspaceStatusTests.swift +2 -3
- package/Tests/RunicTests/InstallOriginTests.swift +1 -2
- package/Tests/RunicTests/KeyboardShortcutsBundleTests.swift +2 -2
- package/Tests/RunicTests/LiveAccountTests.swift +3 -3
- package/Tests/RunicTests/LoadingPatternTests.swift +3 -4
- package/Tests/RunicTests/MenuCardModelTests.swift +33 -34
- package/Tests/RunicTests/MiniMaxCookieHeaderStoreTestSupport.swift +4 -1
- package/Tests/RunicTests/MiniMaxGroupIDStoreTestSupport.swift +4 -1
- package/Tests/RunicTests/MiniMaxParsingTests.swift +4 -5
- package/Tests/RunicTests/MiniMaxTokenStoreTestSupport.swift +4 -1
- package/Tests/RunicTests/OTelGenAILedgerAdapterTests.swift +4 -6
- package/Tests/RunicTests/OpenAIDashboardBrowserCookieImporterTests.swift +1 -2
- package/Tests/RunicTests/OpenAIDashboardFetcherCreditsWaitTests.swift +5 -6
- package/Tests/RunicTests/OpenAIDashboardOffscreenHostTests.swift +2 -3
- package/Tests/RunicTests/OpenAIDashboardParserTests.swift +12 -13
- package/Tests/RunicTests/OpenAIWebAccountSwitchTests.swift +17 -6
- package/Tests/RunicTests/OpenRouterTokenStoreTestSupport.swift +52 -13
- package/Tests/RunicTests/PathBuilderTests.swift +14 -15
- package/Tests/RunicTests/ProviderFetchPipelineTests.swift +3 -4
- package/Tests/RunicTests/ProviderIconResourcesTests.swift +1 -2
- package/Tests/RunicTests/ProviderMetadataStatusLinkTests.swift +1 -2
- package/Tests/RunicTests/ProviderSettingsDescriptorTests.swift +53 -19
- package/Tests/RunicTests/ProviderToggleStoreTests.swift +10 -11
- package/Tests/RunicTests/RunicTests.swift +6 -7
- package/Tests/RunicTests/SessionQuotaNotificationLogicTests.swift +5 -6
- package/Tests/RunicTests/SettingsStoreTests.swift +149 -45
- package/Tests/RunicTests/StatusItemAnimationTests.swift +8 -3
- package/Tests/RunicTests/StatusMenuTests.swift +63 -22
- package/Tests/RunicTests/StatusProbeTests.swift +28 -29
- package/Tests/RunicTests/SubprocessRunnerTests.swift +1 -3
- package/Tests/RunicTests/SubscriptionDetectionTests.swift +8 -9
- package/Tests/RunicTests/TTYCommandRunnerTests.swift +8 -9
- package/Tests/RunicTests/TeamShowbackExporterTests.swift +2 -4
- package/Tests/RunicTests/TextParsingTests.swift +1 -2
- package/Tests/RunicTests/UsageFormatterTests.swift +15 -16
- package/Tests/RunicTests/UsageLedgerAnomalyDetectionTests.swift +12 -14
- package/Tests/RunicTests/UsageLedgerForecastTests.swift +4 -6
- package/Tests/RunicTests/UsageLedgerInsightsAdvisorTests.swift +7 -9
- package/Tests/RunicTests/UsageLedgerProjectNameTests.swift +6 -8
- package/Tests/RunicTests/UsagePaceTests.swift +5 -6
- package/Tests/RunicTests/UsagePaceTextTests.swift +8 -9
- package/Tests/RunicTests/UsagePolicyEngineTests.swift +3 -5
- package/Tests/RunicTests/WidgetSnapshotTests.swift +1 -3
- package/Tests/RunicTests/ZaiProviderTests.swift +3 -5
- package/Tests/RunicTests/ZaiTokenStoreTestSupport.swift +4 -1
- package/appcast.xml +36 -37
- package/package.json +1 -1
- package/runic.png +0 -0
- package/runic.svg +15 -0
- package/version.env +2 -2
|
@@ -14,11 +14,11 @@ struct ProjectBudgetMenuView: View {
|
|
|
14
14
|
|
|
15
15
|
var statusColor: Color {
|
|
16
16
|
if self.isOverBudget {
|
|
17
|
-
|
|
17
|
+
Color(red: 0.94, green: 0.36, blue: 0.36) // Red
|
|
18
18
|
} else if self.isNearLimit {
|
|
19
|
-
|
|
19
|
+
Color(red: 0.94, green: 0.74, blue: 0.26) // Yellow
|
|
20
20
|
} else {
|
|
21
|
-
|
|
21
|
+
Color(red: 0.46, green: 0.75, blue: 0.36) // Green
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
}
|
|
@@ -44,7 +44,7 @@ struct ProjectBudgetMenuView: View {
|
|
|
44
44
|
VStack(alignment: .leading, spacing: RunicSpacing.sm) {
|
|
45
45
|
HStack {
|
|
46
46
|
Text("Project Budgets")
|
|
47
|
-
.font(.headline)
|
|
47
|
+
.font(RunicFont.headline)
|
|
48
48
|
.fontWeight(.semibold)
|
|
49
49
|
Spacer()
|
|
50
50
|
Button {
|
|
@@ -52,9 +52,9 @@ struct ProjectBudgetMenuView: View {
|
|
|
52
52
|
} label: {
|
|
53
53
|
HStack(spacing: RunicSpacing.xxs) {
|
|
54
54
|
Image(systemName: "gearshape")
|
|
55
|
-
.font(.caption)
|
|
55
|
+
.font(RunicFont.caption)
|
|
56
56
|
Text("Configure")
|
|
57
|
-
.font(.caption)
|
|
57
|
+
.font(RunicFont.caption)
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
.buttonStyle(.plain)
|
|
@@ -64,13 +64,13 @@ struct ProjectBudgetMenuView: View {
|
|
|
64
64
|
if model.isEmpty {
|
|
65
65
|
VStack(alignment: .leading, spacing: RunicSpacing.xs) {
|
|
66
66
|
Text("No budgets configured.")
|
|
67
|
-
.font(.footnote)
|
|
67
|
+
.font(RunicFont.footnote)
|
|
68
68
|
.foregroundStyle(.secondary)
|
|
69
69
|
Button {
|
|
70
70
|
self.onOpenPreferences()
|
|
71
71
|
} label: {
|
|
72
72
|
Text("Set up budgets in Preferences")
|
|
73
|
-
.font(.caption)
|
|
73
|
+
.font(RunicFont.caption)
|
|
74
74
|
}
|
|
75
75
|
.buttonStyle(.plain)
|
|
76
76
|
.foregroundStyle(.blue)
|
|
@@ -88,8 +88,8 @@ struct ProjectBudgetMenuView: View {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
// Summary
|
|
91
|
-
let overBudget = model.
|
|
92
|
-
let nearLimit = model.
|
|
91
|
+
let overBudget = model.count(where: { $0.isOverBudget })
|
|
92
|
+
let nearLimit = model.count(where: { $0.isNearLimit && !$0.isOverBudget })
|
|
93
93
|
if overBudget > 0 || nearLimit > 0 {
|
|
94
94
|
Divider()
|
|
95
95
|
.padding(.vertical, RunicSpacing.xxs)
|
|
@@ -98,20 +98,20 @@ struct ProjectBudgetMenuView: View {
|
|
|
98
98
|
if overBudget > 0 {
|
|
99
99
|
HStack(spacing: RunicSpacing.xxs) {
|
|
100
100
|
Image(systemName: "exclamationmark.triangle.fill")
|
|
101
|
-
.font(.caption2)
|
|
101
|
+
.font(RunicFont.caption2)
|
|
102
102
|
.foregroundStyle(Color(red: 0.94, green: 0.36, blue: 0.36))
|
|
103
103
|
Text("\(overBudget) project\(overBudget == 1 ? "" : "s") over budget")
|
|
104
|
-
.font(.caption)
|
|
104
|
+
.font(RunicFont.caption)
|
|
105
105
|
.foregroundStyle(.secondary)
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
if nearLimit > 0 {
|
|
109
109
|
HStack(spacing: RunicSpacing.xxs) {
|
|
110
110
|
Image(systemName: "exclamationmark.circle.fill")
|
|
111
|
-
.font(.caption2)
|
|
111
|
+
.font(RunicFont.caption2)
|
|
112
112
|
.foregroundStyle(Color(red: 0.94, green: 0.74, blue: 0.26))
|
|
113
113
|
Text("\(nearLimit) project\(nearLimit == 1 ? "" : "s") near limit")
|
|
114
|
-
.font(.caption)
|
|
114
|
+
.font(RunicFont.caption)
|
|
115
115
|
.foregroundStyle(.secondary)
|
|
116
116
|
}
|
|
117
117
|
}
|
|
@@ -167,34 +167,34 @@ private struct ProjectBudgetRow: View {
|
|
|
167
167
|
var body: some View {
|
|
168
168
|
VStack(alignment: .leading, spacing: RunicSpacing.xxs) {
|
|
169
169
|
HStack(alignment: .firstTextBaseline) {
|
|
170
|
-
Text(project.projectName)
|
|
171
|
-
.font(.body)
|
|
170
|
+
Text(self.project.projectName)
|
|
171
|
+
.font(RunicFont.body)
|
|
172
172
|
.fontWeight(.medium)
|
|
173
173
|
.lineLimit(1)
|
|
174
174
|
Spacer()
|
|
175
175
|
Text(self.budgetText)
|
|
176
|
-
.font(.caption)
|
|
176
|
+
.font(RunicFont.caption)
|
|
177
177
|
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
UsageProgressBar(
|
|
181
|
-
percent: project.percentUsed,
|
|
182
|
-
tint: project.statusColor,
|
|
181
|
+
percent: self.project.percentUsed,
|
|
182
|
+
tint: self.project.statusColor,
|
|
183
183
|
accessibilityLabel: "Budget usage")
|
|
184
184
|
|
|
185
185
|
HStack(alignment: .firstTextBaseline) {
|
|
186
|
-
Text(String(format: "%.0f%%", project.percentUsed))
|
|
187
|
-
.font(.caption)
|
|
186
|
+
Text(String(format: "%.0f%%", self.project.percentUsed))
|
|
187
|
+
.font(RunicFont.caption)
|
|
188
188
|
.foregroundStyle(self.statusTextColor)
|
|
189
189
|
Spacer()
|
|
190
|
-
if project.isOverBudget {
|
|
190
|
+
if self.project.isOverBudget {
|
|
191
191
|
Text("Over budget")
|
|
192
|
-
.font(.caption)
|
|
192
|
+
.font(RunicFont.caption)
|
|
193
193
|
.foregroundStyle(Color(red: 0.94, green: 0.36, blue: 0.36))
|
|
194
194
|
.fontWeight(.medium)
|
|
195
|
-
} else if project.isNearLimit {
|
|
195
|
+
} else if self.project.isNearLimit {
|
|
196
196
|
Text("Near limit")
|
|
197
|
-
.font(.caption)
|
|
197
|
+
.font(RunicFont.caption)
|
|
198
198
|
.foregroundStyle(Color(red: 0.94, green: 0.74, blue: 0.26))
|
|
199
199
|
.fontWeight(.medium)
|
|
200
200
|
}
|
|
@@ -208,13 +208,13 @@ private struct ProjectBudgetRow: View {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
private var budgetText: String {
|
|
211
|
-
"\(UsageFormatter.usdString(project.spent)) / \(UsageFormatter.usdString(project.budget.monthlyLimit))"
|
|
211
|
+
"\(UsageFormatter.usdString(self.project.spent)) / \(UsageFormatter.usdString(self.project.budget.monthlyLimit))"
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
private var statusTextColor: Color {
|
|
215
215
|
if self.isHighlighted {
|
|
216
216
|
return MenuHighlightStyle.selectionText
|
|
217
217
|
}
|
|
218
|
-
return project.statusColor
|
|
218
|
+
return self.project.statusColor
|
|
219
219
|
}
|
|
220
220
|
}
|
|
@@ -33,11 +33,11 @@ struct ProviderTabBarView: View {
|
|
|
33
33
|
.aspectRatio(contentMode: .fit)
|
|
34
34
|
.frame(width: 18, height: 18)
|
|
35
35
|
} else if tab.provider == nil {
|
|
36
|
-
|
|
36
|
+
Image(systemName: "square.grid.2x2")
|
|
37
37
|
.font(.system(size: 14, weight: .medium))
|
|
38
38
|
}
|
|
39
39
|
Text(tab.label)
|
|
40
|
-
.font(.
|
|
40
|
+
.font(RunicFont.caption)
|
|
41
41
|
.fontWeight(tab.isSelected ? .semibold : .regular)
|
|
42
42
|
.lineLimit(1)
|
|
43
43
|
}
|
|
@@ -47,16 +47,14 @@ struct ProviderTabBarView: View {
|
|
|
47
47
|
Capsule(style: .continuous)
|
|
48
48
|
.fill(tab.isSelected
|
|
49
49
|
? tab.brandColor.opacity(RunicColors.Opacity.medium)
|
|
50
|
-
: Color(nsColor: .controlBackgroundColor).opacity(RunicColors.Opacity.subtle))
|
|
51
|
-
)
|
|
50
|
+
: Color(nsColor: .controlBackgroundColor).opacity(RunicColors.Opacity.subtle)))
|
|
52
51
|
.overlay(
|
|
53
52
|
Capsule(style: .continuous)
|
|
54
53
|
.stroke(
|
|
55
54
|
tab.isSelected
|
|
56
55
|
? tab.brandColor.opacity(RunicColors.Opacity.strong)
|
|
57
56
|
: Color(nsColor: .separatorColor).opacity(RunicColors.Opacity.light),
|
|
58
|
-
lineWidth: 0.5)
|
|
59
|
-
)
|
|
57
|
+
lineWidth: 0.5))
|
|
60
58
|
.foregroundStyle(tab.isSelected ? tab.brandColor : .secondary)
|
|
61
59
|
.shadow(
|
|
62
60
|
color: tab.isSelected ? tab.brandColor.opacity(0.15) : .clear,
|
|
@@ -38,7 +38,7 @@ struct SubscriptionUtilizationChartMenuView: View {
|
|
|
38
38
|
self.width = width
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
private static let barColor = RunicColors.chartColor(at: 4)
|
|
41
|
+
private static let barColor = RunicColors.chartColor(at: 4) // teal
|
|
42
42
|
private static let wastedColor = Color(nsColor: .systemGray).opacity(0.2)
|
|
43
43
|
|
|
44
44
|
var body: some View {
|
|
@@ -50,7 +50,7 @@ struct SubscriptionUtilizationChartMenuView: View {
|
|
|
50
50
|
|
|
51
51
|
VStack(alignment: .leading, spacing: RunicSpacing.xs) {
|
|
52
52
|
Text("Utilization")
|
|
53
|
-
.font(.
|
|
53
|
+
.font(RunicFont.subheadline)
|
|
54
54
|
.fontWeight(.semibold)
|
|
55
55
|
|
|
56
56
|
Picker("", selection: self.$selectedPeriod) {
|
|
@@ -62,7 +62,7 @@ struct SubscriptionUtilizationChartMenuView: View {
|
|
|
62
62
|
|
|
63
63
|
if model.bars.isEmpty {
|
|
64
64
|
Text("No utilization data available.")
|
|
65
|
-
.font(.
|
|
65
|
+
.font(RunicFont.footnote)
|
|
66
66
|
.foregroundStyle(.secondary)
|
|
67
67
|
.frame(height: 80)
|
|
68
68
|
} else {
|
|
@@ -83,7 +83,7 @@ struct SubscriptionUtilizationChartMenuView: View {
|
|
|
83
83
|
AxisValueLabel {
|
|
84
84
|
if let pct = value.as(Int.self) {
|
|
85
85
|
Text("\(pct)%")
|
|
86
|
-
.font(.
|
|
86
|
+
.font(RunicFont.caption2)
|
|
87
87
|
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
|
|
88
88
|
}
|
|
89
89
|
}
|
|
@@ -92,7 +92,7 @@ struct SubscriptionUtilizationChartMenuView: View {
|
|
|
92
92
|
.chartXAxis {
|
|
93
93
|
AxisMarks(values: .automatic(desiredCount: 5)) { _ in
|
|
94
94
|
AxisValueLabel()
|
|
95
|
-
.font(.
|
|
95
|
+
.font(RunicFont.caption2)
|
|
96
96
|
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
|
|
97
97
|
}
|
|
98
98
|
}
|
|
@@ -104,7 +104,7 @@ struct SubscriptionUtilizationChartMenuView: View {
|
|
|
104
104
|
let used = Int(latest.usedPercent.rounded())
|
|
105
105
|
let unused = max(0, 100 - used)
|
|
106
106
|
Text("\(latest.label): \(used)% used, \(unused)% unused")
|
|
107
|
-
.font(.
|
|
107
|
+
.font(RunicFont.caption)
|
|
108
108
|
.foregroundStyle(.secondary)
|
|
109
109
|
}
|
|
110
110
|
}
|
|
@@ -159,7 +159,8 @@ struct SubscriptionUtilizationChartMenuView: View {
|
|
|
159
159
|
// Aggregate by ISO week
|
|
160
160
|
var weekBuckets: [(label: String, tokens: Int, date: Date, isThisWeek: Bool)] = []
|
|
161
161
|
for weekOffset in stride(from: -3, through: 0, by: 1) {
|
|
162
|
-
guard let weekStart = calendar.date(byAdding: .weekOfYear, value: weekOffset, to: todayStart)
|
|
162
|
+
guard let weekStart = calendar.date(byAdding: .weekOfYear, value: weekOffset, to: todayStart)
|
|
163
|
+
else { continue }
|
|
163
164
|
var total = 0
|
|
164
165
|
for dayOffset in 0..<7 {
|
|
165
166
|
guard let date = calendar.date(byAdding: .day, value: dayOffset, to: weekStart) else { continue }
|
|
@@ -171,20 +172,27 @@ struct SubscriptionUtilizationChartMenuView: View {
|
|
|
171
172
|
let maxWeekTokens = weekBuckets.map(\.tokens).max() ?? 1
|
|
172
173
|
let bars = weekBuckets.map { bucket in
|
|
173
174
|
let pct = maxWeekTokens > 0 ? Double(bucket.tokens) / Double(maxWeekTokens) * 100 : 0
|
|
174
|
-
return UtilizationBar(
|
|
175
|
+
return UtilizationBar(
|
|
176
|
+
id: bucket.label,
|
|
177
|
+
label: bucket.label,
|
|
178
|
+
date: bucket.date,
|
|
179
|
+
usedPercent: pct,
|
|
180
|
+
isToday: bucket.isThisWeek)
|
|
175
181
|
}
|
|
176
182
|
return UtilizationModel(bars: bars)
|
|
177
183
|
|
|
178
184
|
case .monthly:
|
|
179
185
|
var monthBuckets: [(label: String, tokens: Int, date: Date, isThisMonth: Bool)] = []
|
|
180
186
|
for monthOffset in stride(from: -2, through: 0, by: 1) {
|
|
181
|
-
guard let monthStart = calendar.date(byAdding: .month, value: monthOffset, to: todayStart)
|
|
187
|
+
guard let monthStart = calendar.date(byAdding: .month, value: monthOffset, to: todayStart)
|
|
188
|
+
else { continue }
|
|
182
189
|
let monthComponents = calendar.dateComponents([.year, .month], from: monthStart)
|
|
183
190
|
guard let actualMonthStart = calendar.date(from: monthComponents),
|
|
184
191
|
let range = calendar.range(of: .day, in: .month, for: actualMonthStart) else { continue }
|
|
185
192
|
var total = 0
|
|
186
193
|
for dayOffset in 0..<range.count {
|
|
187
|
-
guard let date = calendar.date(byAdding: .day, value: dayOffset, to: actualMonthStart)
|
|
194
|
+
guard let date = calendar.date(byAdding: .day, value: dayOffset, to: actualMonthStart)
|
|
195
|
+
else { continue }
|
|
188
196
|
total += tokensByDay[formatter.string(from: date)] ?? 0
|
|
189
197
|
}
|
|
190
198
|
let label = monthStart.formatted(.dateTime.month(.abbreviated))
|
|
@@ -193,7 +201,12 @@ struct SubscriptionUtilizationChartMenuView: View {
|
|
|
193
201
|
let maxMonthTokens = monthBuckets.map(\.tokens).max() ?? 1
|
|
194
202
|
let bars = monthBuckets.map { bucket in
|
|
195
203
|
let pct = maxMonthTokens > 0 ? Double(bucket.tokens) / Double(maxMonthTokens) * 100 : 0
|
|
196
|
-
return UtilizationBar(
|
|
204
|
+
return UtilizationBar(
|
|
205
|
+
id: bucket.label,
|
|
206
|
+
label: bucket.label,
|
|
207
|
+
date: bucket.date,
|
|
208
|
+
usedPercent: pct,
|
|
209
|
+
isToday: bucket.isThisMonth)
|
|
197
210
|
}
|
|
198
211
|
return UtilizationModel(bars: bars)
|
|
199
212
|
}
|
|
@@ -32,7 +32,7 @@ struct UsageBreakdownChartMenuView: View {
|
|
|
32
32
|
VStack(alignment: .leading, spacing: RunicSpacing.sm) {
|
|
33
33
|
if model.points.isEmpty {
|
|
34
34
|
Text("No usage breakdown data.")
|
|
35
|
-
.font(.footnote)
|
|
35
|
+
.font(RunicFont.footnote)
|
|
36
36
|
.foregroundStyle(.secondary)
|
|
37
37
|
} else {
|
|
38
38
|
Chart {
|
|
@@ -58,7 +58,7 @@ struct UsageBreakdownChartMenuView: View {
|
|
|
58
58
|
AxisGridLine().foregroundStyle(Color.clear)
|
|
59
59
|
AxisTick().foregroundStyle(Color.clear)
|
|
60
60
|
AxisValueLabel(format: .dateTime.month(.abbreviated).day())
|
|
61
|
-
.font(.caption2)
|
|
61
|
+
.font(RunicFont.caption2)
|
|
62
62
|
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -88,13 +88,13 @@ struct UsageBreakdownChartMenuView: View {
|
|
|
88
88
|
let detail = self.detailLines(model: model)
|
|
89
89
|
VStack(alignment: .leading, spacing: 0) {
|
|
90
90
|
Text(detail.primary)
|
|
91
|
-
.font(.
|
|
91
|
+
.font(RunicFont.caption)
|
|
92
92
|
.foregroundStyle(.secondary)
|
|
93
93
|
.lineLimit(1)
|
|
94
94
|
.truncationMode(.tail)
|
|
95
95
|
.frame(height: 16, alignment: .leading)
|
|
96
96
|
Text(detail.secondary ?? " ")
|
|
97
|
-
.font(.
|
|
97
|
+
.font(RunicFont.caption)
|
|
98
98
|
.foregroundStyle(.secondary)
|
|
99
99
|
.lineLimit(1)
|
|
100
100
|
.truncationMode(.tail)
|
|
@@ -113,7 +113,7 @@ struct UsageBreakdownChartMenuView: View {
|
|
|
113
113
|
.fill(model.color(for: service))
|
|
114
114
|
.frame(width: RunicSpacing.chartLegendDot, height: RunicSpacing.chartLegendDot)
|
|
115
115
|
Text(service)
|
|
116
|
-
.font(.caption2)
|
|
116
|
+
.font(RunicFont.caption2)
|
|
117
117
|
.foregroundStyle(.secondary)
|
|
118
118
|
.lineLimit(1)
|
|
119
119
|
}
|
|
@@ -46,12 +46,12 @@ struct UsageHeatmapMenuView: View {
|
|
|
46
46
|
let model = Self.makeModel(from: self.hourlySummaries)
|
|
47
47
|
VStack(alignment: .leading, spacing: RunicSpacing.sm) {
|
|
48
48
|
Text("Usage Heatmap (24×7)")
|
|
49
|
-
.font(.headline)
|
|
49
|
+
.font(RunicFont.headline)
|
|
50
50
|
.fontWeight(.semibold)
|
|
51
51
|
|
|
52
52
|
if model.isEmpty {
|
|
53
53
|
Text("No heatmap data.")
|
|
54
|
-
.font(.footnote)
|
|
54
|
+
.font(RunicFont.footnote)
|
|
55
55
|
.foregroundStyle(.secondary)
|
|
56
56
|
} else {
|
|
57
57
|
ScrollView(.horizontal, showsIndicators: true) {
|
|
@@ -62,7 +62,7 @@ struct UsageHeatmapMenuView: View {
|
|
|
62
62
|
.frame(width: 40, alignment: .leading)
|
|
63
63
|
ForEach(0..<24, id: \.self) { hour in
|
|
64
64
|
Text(self.hourLabel(hour))
|
|
65
|
-
.font(.caption2)
|
|
65
|
+
.font(RunicFont.caption2)
|
|
66
66
|
.foregroundStyle(.secondary)
|
|
67
67
|
.frame(width: 40, alignment: .center)
|
|
68
68
|
.lineLimit(1)
|
|
@@ -73,7 +73,7 @@ struct UsageHeatmapMenuView: View {
|
|
|
73
73
|
ForEach(0..<7, id: \.self) { weekday in
|
|
74
74
|
HStack(spacing: 2) {
|
|
75
75
|
Text(self.weekdayLabel(weekday))
|
|
76
|
-
.font(.caption)
|
|
76
|
+
.font(RunicFont.caption)
|
|
77
77
|
.foregroundStyle(.secondary)
|
|
78
78
|
.frame(width: 38, alignment: .leading)
|
|
79
79
|
|
|
@@ -99,7 +99,7 @@ struct UsageHeatmapMenuView: View {
|
|
|
99
99
|
// Legend
|
|
100
100
|
HStack(spacing: RunicSpacing.xs) {
|
|
101
101
|
Text("Low")
|
|
102
|
-
.font(.caption2)
|
|
102
|
+
.font(RunicFont.caption2)
|
|
103
103
|
.foregroundStyle(.secondary)
|
|
104
104
|
HStack(spacing: 2) {
|
|
105
105
|
ForEach(0..<5, id: \.self) { level in
|
|
@@ -110,7 +110,7 @@ struct UsageHeatmapMenuView: View {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
Text("High")
|
|
113
|
-
.font(.caption2)
|
|
113
|
+
.font(RunicFont.caption2)
|
|
114
114
|
.foregroundStyle(.secondary)
|
|
115
115
|
}
|
|
116
116
|
.padding(.top, RunicSpacing.xs)
|
|
@@ -121,7 +121,7 @@ struct UsageHeatmapMenuView: View {
|
|
|
121
121
|
|
|
122
122
|
let detail = self.detailText(model: model)
|
|
123
123
|
Text(detail)
|
|
124
|
-
.font(.caption)
|
|
124
|
+
.font(RunicFont.caption)
|
|
125
125
|
.foregroundStyle(.secondary)
|
|
126
126
|
.lineLimit(2)
|
|
127
127
|
.frame(height: 32, alignment: .leading)
|
|
@@ -229,4 +229,3 @@ private struct HeatmapCellView: View {
|
|
|
229
229
|
.cornerRadius(RunicCornerRadius.xs)
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
|
-
|
|
@@ -5,30 +5,29 @@ import SwiftUI
|
|
|
5
5
|
@MainActor
|
|
6
6
|
struct UsageTimelineChartMenuView: View {
|
|
7
7
|
enum TimeRange: String, CaseIterable {
|
|
8
|
-
case
|
|
9
|
-
case sixHours = "6h"
|
|
10
|
-
case oneDay = "1d"
|
|
8
|
+
case threeDays = "3d"
|
|
11
9
|
case sevenDays = "7d"
|
|
12
10
|
case thirtyDays = "30d"
|
|
11
|
+
case quarter = "90d"
|
|
12
|
+
case year = "1y"
|
|
13
13
|
|
|
14
14
|
var cutoffInterval: TimeInterval {
|
|
15
15
|
switch self {
|
|
16
|
-
case .
|
|
17
|
-
case .
|
|
18
|
-
case .
|
|
19
|
-
case .
|
|
20
|
-
case .
|
|
16
|
+
case .threeDays: -259_200
|
|
17
|
+
case .sevenDays: -604_800
|
|
18
|
+
case .thirtyDays: -2_592_000
|
|
19
|
+
case .quarter: -7_776_000
|
|
20
|
+
case .year: -31_536_000
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
var usesHourlyData: Bool {
|
|
25
|
-
|
|
26
|
-
case .oneHour, .sixHours, .oneDay: return true
|
|
27
|
-
default: return false
|
|
28
|
-
}
|
|
25
|
+
false
|
|
29
26
|
}
|
|
30
27
|
|
|
31
|
-
var label: String {
|
|
28
|
+
var label: String {
|
|
29
|
+
self.rawValue
|
|
30
|
+
}
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
private struct Point: Identifiable {
|
|
@@ -48,7 +47,7 @@ struct UsageTimelineChartMenuView: View {
|
|
|
48
47
|
private let dailySummaries: [UsageLedgerDailySummary]
|
|
49
48
|
private let hourlySummaries: [UsageLedgerHourlySummary]
|
|
50
49
|
private let width: CGFloat
|
|
51
|
-
@State private var selectedTimeRange: TimeRange = .
|
|
50
|
+
@State private var selectedTimeRange: TimeRange = .sevenDays
|
|
52
51
|
@State private var selectedKey: String?
|
|
53
52
|
|
|
54
53
|
init(
|
|
@@ -65,14 +64,12 @@ struct UsageTimelineChartMenuView: View {
|
|
|
65
64
|
private static let selectionBandColor = Color(nsColor: .labelColor).opacity(0.1)
|
|
66
65
|
|
|
67
66
|
var body: some View {
|
|
68
|
-
let model = self.selectedTimeRange
|
|
69
|
-
? Self.makeHourlyModel(from: self.hourlySummaries, timeRange: self.selectedTimeRange)
|
|
70
|
-
: Self.makeDailyModel(from: self.dailySummaries, timeRange: self.selectedTimeRange)
|
|
67
|
+
let model = Self.makeDailyModel(from: self.dailySummaries, timeRange: self.selectedTimeRange)
|
|
71
68
|
|
|
72
69
|
VStack(alignment: .leading, spacing: RunicSpacing.xs) {
|
|
73
70
|
// Header on its own line
|
|
74
71
|
Text("Timeline")
|
|
75
|
-
.font(.
|
|
72
|
+
.font(RunicFont.subheadline)
|
|
76
73
|
.fontWeight(.semibold)
|
|
77
74
|
|
|
78
75
|
// Range picker on its own line
|
|
@@ -85,7 +82,7 @@ struct UsageTimelineChartMenuView: View {
|
|
|
85
82
|
|
|
86
83
|
if model.points.isEmpty {
|
|
87
84
|
Text("No data for selected range.")
|
|
88
|
-
.font(.
|
|
85
|
+
.font(RunicFont.footnote)
|
|
89
86
|
.foregroundStyle(.secondary)
|
|
90
87
|
.frame(height: 100)
|
|
91
88
|
} else {
|
|
@@ -119,7 +116,7 @@ struct UsageTimelineChartMenuView: View {
|
|
|
119
116
|
.symbolSize(60)
|
|
120
117
|
.annotation(position: .top, spacing: 4) {
|
|
121
118
|
Text("Peak")
|
|
122
|
-
.font(.
|
|
119
|
+
.font(RunicFont.caption2)
|
|
123
120
|
.foregroundStyle(Color(nsColor: .systemYellow))
|
|
124
121
|
}
|
|
125
122
|
}
|
|
@@ -131,7 +128,7 @@ struct UsageTimelineChartMenuView: View {
|
|
|
131
128
|
AxisValueLabel {
|
|
132
129
|
if let tokens = value.as(Int.self) {
|
|
133
130
|
Text(UsageFormatter.tokenCountString(tokens))
|
|
134
|
-
.font(.
|
|
131
|
+
.font(RunicFont.caption2)
|
|
135
132
|
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
|
|
136
133
|
}
|
|
137
134
|
}
|
|
@@ -142,7 +139,7 @@ struct UsageTimelineChartMenuView: View {
|
|
|
142
139
|
AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5, dash: [4, 3]))
|
|
143
140
|
.foregroundStyle(Color(nsColor: .separatorColor).opacity(0.3))
|
|
144
141
|
AxisValueLabel(format: model.xAxisFormat)
|
|
145
|
-
.font(.
|
|
142
|
+
.font(RunicFont.caption2)
|
|
146
143
|
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
|
|
147
144
|
}
|
|
148
145
|
}
|
|
@@ -170,7 +167,7 @@ struct UsageTimelineChartMenuView: View {
|
|
|
170
167
|
// Detail line
|
|
171
168
|
let detail = self.detailText(model: model)
|
|
172
169
|
Text(detail)
|
|
173
|
-
.font(.
|
|
170
|
+
.font(RunicFont.caption)
|
|
174
171
|
.foregroundStyle(.secondary)
|
|
175
172
|
.lineLimit(1)
|
|
176
173
|
.truncationMode(.tail)
|
|
@@ -191,7 +188,8 @@ struct UsageTimelineChartMenuView: View {
|
|
|
191
188
|
}
|
|
192
189
|
.chartPanelStyle(width: self.width)
|
|
193
190
|
.accessibilityElement(children: .ignore)
|
|
194
|
-
.accessibilityLabel(
|
|
191
|
+
.accessibilityLabel(
|
|
192
|
+
"Usage timeline chart showing \(model.points.count) data points over \(self.selectedTimeRange.label)")
|
|
195
193
|
}
|
|
196
194
|
|
|
197
195
|
// MARK: - Stat cell
|
|
@@ -203,10 +201,10 @@ struct UsageTimelineChartMenuView: View {
|
|
|
203
201
|
var body: some View {
|
|
204
202
|
VStack(alignment: .leading, spacing: RunicSpacing.xxxs) {
|
|
205
203
|
Text(self.label)
|
|
206
|
-
.font(.
|
|
204
|
+
.font(RunicFont.caption2)
|
|
207
205
|
.foregroundStyle(.tertiary)
|
|
208
206
|
Text(self.value)
|
|
209
|
-
.font(.
|
|
207
|
+
.font(RunicFont.caption)
|
|
210
208
|
.fontWeight(.medium)
|
|
211
209
|
}
|
|
212
210
|
}
|
|
@@ -261,10 +259,12 @@ struct UsageTimelineChartMenuView: View {
|
|
|
261
259
|
}
|
|
262
260
|
}
|
|
263
261
|
|
|
264
|
-
let desiredAxisCount
|
|
265
|
-
case .
|
|
262
|
+
let desiredAxisCount = switch timeRange {
|
|
263
|
+
case .threeDays: 3
|
|
266
264
|
case .sevenDays: 5
|
|
267
265
|
case .thirtyDays: 6
|
|
266
|
+
case .quarter: 6
|
|
267
|
+
case .year: 6
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
return Model(
|
|
@@ -281,54 +281,6 @@ struct UsageTimelineChartMenuView: View {
|
|
|
281
281
|
isHourly: false)
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
// MARK: - Hourly model
|
|
285
|
-
|
|
286
|
-
private static func makeHourlyModel(from summaries: [UsageLedgerHourlySummary], timeRange: TimeRange) -> Model {
|
|
287
|
-
let now = Date()
|
|
288
|
-
let cutoffDate = now.addingTimeInterval(timeRange.cutoffInterval)
|
|
289
|
-
let filtered = summaries.filter { $0.hourStart >= cutoffDate }
|
|
290
|
-
let sorted = filtered.sorted { $0.hourStart < $1.hourStart }
|
|
291
|
-
|
|
292
|
-
var points: [Point] = []
|
|
293
|
-
var pointsByKey: [String: Point] = [:]
|
|
294
|
-
var dateKeys: [(key: String, date: Date)] = []
|
|
295
|
-
var peak: Point?
|
|
296
|
-
var totalTokens = 0
|
|
297
|
-
|
|
298
|
-
for summary in sorted {
|
|
299
|
-
let tokens = summary.totals.totalTokens
|
|
300
|
-
totalTokens += tokens
|
|
301
|
-
let point = Point(date: summary.hourStart, totalTokens: tokens, key: summary.hourKey)
|
|
302
|
-
points.append(point)
|
|
303
|
-
pointsByKey[summary.hourKey] = point
|
|
304
|
-
dateKeys.append((summary.hourKey, summary.hourStart))
|
|
305
|
-
|
|
306
|
-
if peak == nil || tokens > (peak?.totalTokens ?? 0) {
|
|
307
|
-
peak = point
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
let desiredAxisCount: Int = switch timeRange {
|
|
312
|
-
case .oneHour: 4
|
|
313
|
-
case .sixHours: 6
|
|
314
|
-
case .oneDay: 8
|
|
315
|
-
default: 6
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return Model(
|
|
319
|
-
points: points,
|
|
320
|
-
pointsByKey: pointsByKey,
|
|
321
|
-
dateKeys: dateKeys,
|
|
322
|
-
peakPoint: peak,
|
|
323
|
-
totalTokens: totalTokens,
|
|
324
|
-
averagePerPeriod: points.isEmpty ? 0 : totalTokens / max(1, points.count),
|
|
325
|
-
peakTokens: peak?.totalTokens ?? 0,
|
|
326
|
-
totalCostUSD: nil,
|
|
327
|
-
desiredAxisCount: desiredAxisCount,
|
|
328
|
-
xAxisFormat: .dateTime.hour(.defaultDigits(amPM: .narrow)),
|
|
329
|
-
isHourly: true)
|
|
330
|
-
}
|
|
331
|
-
|
|
332
284
|
// MARK: - Interaction
|
|
333
285
|
|
|
334
286
|
private func selectionBandRect(model: Model, proxy: ChartProxy, geo: GeometryProxy) -> CGRect? {
|
|
@@ -49,12 +49,12 @@ struct UsageWindowComparisonChartMenuView: View {
|
|
|
49
49
|
|
|
50
50
|
VStack(alignment: .leading, spacing: RunicSpacing.xs) {
|
|
51
51
|
Text("Windows")
|
|
52
|
-
.font(.
|
|
52
|
+
.font(RunicFont.subheadline)
|
|
53
53
|
.fontWeight(.semibold)
|
|
54
54
|
|
|
55
55
|
if model.points.isEmpty {
|
|
56
56
|
Text("No usage window data available.")
|
|
57
|
-
.font(.
|
|
57
|
+
.font(RunicFont.footnote)
|
|
58
58
|
.foregroundStyle(.secondary)
|
|
59
59
|
.frame(height: 80)
|
|
60
60
|
} else {
|
|
@@ -64,7 +64,8 @@ struct UsageWindowComparisonChartMenuView: View {
|
|
|
64
64
|
x: .value("Date", point.date),
|
|
65
65
|
y: .value("%", point.percent),
|
|
66
66
|
series: .value("Window", point.series))
|
|
67
|
-
.foregroundStyle(point.series == self.primaryLabel ? Self.primaryColor : Self
|
|
67
|
+
.foregroundStyle(point.series == self.primaryLabel ? Self.primaryColor : Self
|
|
68
|
+
.secondaryColor)
|
|
68
69
|
.lineStyle(StrokeStyle(lineWidth: 2))
|
|
69
70
|
.interpolationMethod(.catmullRom)
|
|
70
71
|
}
|
|
@@ -77,7 +78,7 @@ struct UsageWindowComparisonChartMenuView: View {
|
|
|
77
78
|
AxisValueLabel {
|
|
78
79
|
if let pct = value.as(Int.self) {
|
|
79
80
|
Text("\(pct)%")
|
|
80
|
-
.font(.
|
|
81
|
+
.font(RunicFont.caption2)
|
|
81
82
|
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
|
|
82
83
|
}
|
|
83
84
|
}
|
|
@@ -88,7 +89,7 @@ struct UsageWindowComparisonChartMenuView: View {
|
|
|
88
89
|
AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5, dash: [4, 3]))
|
|
89
90
|
.foregroundStyle(Color(nsColor: .separatorColor).opacity(0.3))
|
|
90
91
|
AxisValueLabel(format: .dateTime.month(.abbreviated).day())
|
|
91
|
-
.font(.
|
|
92
|
+
.font(RunicFont.caption2)
|
|
92
93
|
.foregroundStyle(Color(nsColor: .tertiaryLabelColor))
|
|
93
94
|
}
|
|
94
95
|
}
|
|
@@ -102,7 +103,7 @@ struct UsageWindowComparisonChartMenuView: View {
|
|
|
102
103
|
.fill(Self.primaryColor)
|
|
103
104
|
.frame(width: RunicSpacing.chartLegendDot, height: RunicSpacing.chartLegendDot)
|
|
104
105
|
Text(self.primaryLabel)
|
|
105
|
-
.font(.
|
|
106
|
+
.font(RunicFont.caption2)
|
|
106
107
|
.foregroundStyle(.secondary)
|
|
107
108
|
}
|
|
108
109
|
if let secondaryLabel = self.secondaryLabel {
|
|
@@ -111,7 +112,7 @@ struct UsageWindowComparisonChartMenuView: View {
|
|
|
111
112
|
.fill(Self.secondaryColor)
|
|
112
113
|
.frame(width: RunicSpacing.chartLegendDot, height: RunicSpacing.chartLegendDot)
|
|
113
114
|
Text(secondaryLabel)
|
|
114
|
-
.font(.
|
|
115
|
+
.font(RunicFont.caption2)
|
|
115
116
|
.foregroundStyle(.secondary)
|
|
116
117
|
}
|
|
117
118
|
}
|