This document provides essential information for agentic coding assistants working on this .NET 8.0 WPF application.
- I MUST NEVER create git tags or releases without your explicit instruction
- I MUST NEVER initiate CI/CD release workflows without your permission
- I MUST NEVER create beta or stable releases on my own
- You must explicitly tell me when to create a release
- I will wait for your command before any release-related action
- ALL configured providers MUST be visible in the UI at all times
- NEVER filter out providers just because they have
IsAvailable=false - Providers with missing API keys should show as "Not Available" or "Configure Provider"
- DO NOT wait for fresh data before showing providers - display cached data immediately
- DO NOT show only a subset of providers (e.g., only antigravity) on startup
- The UI must show provider cards immediately, even if data is stale or unavailable
- The goal is to keep as little source code as necessary to deliver required functionality across these applications.
- Prefer deleting redundant layers/wrappers over adding new abstraction when behavior can remain clear and testable.
- New code must justify its existence; avoid duplicate logic paths and unnecessary fallback branches.
- Interfaces are only for types that are mocked in tests. Single-implementation interfaces with no test mocking are unnecessary indirection — use the concrete class directly.
- Never duplicate provider metadata into DB tables. The database stores raw values only; provider definitions (ProviderDefinition class) are the authority for interpretation.
- Provider definitions are immutable per provider ID. If semantics change, a new provider class with a new ID is created.
- When encountering data integrity concerns, do NOT suggest adding columns. Keep the schema minimal.
- Do not give hedged answers like "this will work IF the API returns X." Investigate the actual data (database, logs, live endpoints) and report what IS happening.
- Give definitive "working" or "broken" with evidence. If something is broken, identify the root cause and fix it.
- Never push directly to
mainordevelop: All changes MUST go through feature/fix branches with Pull Requests targetingdevelop. Beta releases targetdevelop; only stable releases targetmain. - Never force push to
mainwithout explicit user permission: If you need to force push to main, ALWAYS ask for confirmation first. - Atomic Commits: Keep commits focused and logically grouped.
- CI/CD Compliance: Ensure that any UI changes or tests are compatible with the headless CI environment.
- No Icons in PRs: When creating pull requests, do not use emojis or icons in the title or body.
- PR Management: ALWAYS modify existing PRs (
gh pr edit) instead of closing and creating new ones. Usegh pr edit --base <branch>to change the target branch. Closing and recreating wastes CI runs and loses conversation context.
Before implementing ANY change, verify the current state first:
- Before version bumps: Run
git tag -l "v*" --sort=-v:refname | head -5to check the latest released tag. Never trust file contents alone — tags may have advanced beyond what's in the files. - Before branch operations: Run
git log --oneline origin/<branch> -5to see what's actually on the remote. - Before releases: Check tags AND file versions AND changelog to ensure consistency.
- Before pushing: Run validation scripts locally:
bash scripts/validate-release-consistency.sh "<version>"for version bumpsdotnet buildfor compilationdotnet testfor test suite
- Before creating PRs: Check the correct target branch (
developfor betas,mainfor stable).
Why: Pushing without verification wastes 5-10 minutes of CI time per failed round-trip and blocks the user.
- AIUsageTracker.Core: Domain models, interfaces, and business logic (PCL)
- AIUsageTracker.Infrastructure: External providers, data access, configuration
- AIUsageTracker.UI.Slim: Lightweight WPF desktop application with compact UI
- AIUsageTracker.Monitor: Background service that collects provider usage data via HTTP API
- AIUsageTracker.CLI: Console interface (cross-platform)
- AIUsageTracker.Web: ASP.NET Core Razor Pages web application for viewing data
- AIUsageTracker.Tests: xUnit unit tests with Moq mocking
# Build entire solution
dotnet build AIUsageTracker.sln --configuration Debug
# Build specific project
dotnet build AIUsageTracker.UI.Slim/AIUsageTracker.UI.Slim.csproj
# Restore dependencies
dotnet restore# Run all unit tests
dotnet test AIUsageTracker.Tests/AIUsageTracker.Tests.csproj --configuration Debug
# Run all tests (no rebuild)
dotnet test --no-build --verbosity normal
# Run a single test
dotnet test --filter "FullyQualifiedName~GetAllUsageAsync_LoadsConfigAndFetchesUsageFromMocks"
# Run tests by class
dotnet test --filter "FullyQualifiedName~ProviderManagerTests"# Run the Monitor service
dotnet run --project AIUsageTracker.Monitor
# Monitor runs on port 5000 by default (auto-discovers available port 5000-5010)
# Port is saved to %LOCALAPPDATA%\AIUsageTracker\monitor.json# Run the Web application (requires Monitor to be running)
dotnet run --project AIUsageTracker.Web
# Web UI runs on port 5100
# Access at http://localhost:5100# Run the Slim WPF application
dotnet run --project AIUsageTracker.UI.Slim
# Automatically discovers Monitor port from monitor.json file
# Falls back to ports 5000-5010 if discovery failsTo generate updated screenshots for documentation (headless and in Privacy Mode):
# Run from the UI bin directory or project root
AIUsageTracker.exe --test --screenshotNote
The --test flag enables explicit UI initialization required for headless rendering. This logic is gated to avoid performance overhead for normal users.
- CI screenshot baseline checks run on
windows-2025; treat CI-rendered artifacts as the final baseline authority. - Keep deterministic screenshot fixture data stable (including fixed clock values) to avoid non-functional image drift.
- If only screenshot baseline fails, download the
slim-ui-screenshotsartifact from the failing run and sync only the drifted files. - Do not add new screenshot files to baseline verification unless workflow and verification script are updated together.
ALWAYS run validation before committing and pushing changes. This prevents CI failures and broken builds.
Run the pre-commit validation script:
./scripts/pre-commit-check.shThis script performs:
- Build Validation - Ensures solution compiles without errors
- Test Execution - Runs all unit tests (162 tests)
- Format Checking - Verifies code matches
.editorconfigrules
Manual validation (if script unavailable):
# Build
dotnet build AIUsageTracker.sln --configuration Release
# Test
dotnet test AIUsageTracker.Tests/AIUsageTracker.Tests.csproj --configuration Release --no-build
# Format check
dotnet format --verify-no-changes --severity warnDo NOT commit if:
- ❌ Build fails
- ❌ Any tests fail
- ❌ Format check shows errors (warnings are OK)
Why this matters:
- CI/CD pipelines will reject broken builds
- Other developers will be blocked by failing tests
- Fixes require additional commits and PR updates
- Wastes CI minutes on known-failing builds
# Publish Windows UI
.\scripts\publish-app.ps1 -Runtime win-x64
# Publish Linux CLI
.\scripts\publish-app.ps1 -Runtime linux-x64- Use file-scoped namespace declarations:
namespace AIUsageTracker.Core.Models; - Place using statements at the top, before namespace declaration
- Group by: System → Third-party → Project references (separated by blank lines)
- Explicitly type
using Microsoft.Extensions.Logging;when needed to avoid ambiguity
- Classes: PascalCase (e.g.,
ProviderManager,AppPreferences) - Methods: PascalCase (e.g.,
GetUsageAsync,LoadConfigAsync) - Properties: PascalCase (e.g.,
ProviderId,IsAvailable) - Private fields: _camelCase with underscore prefix (e.g.,
_httpClient,_logger) - Interfaces: I prefix (e.g.,
IProviderService,IConfigLoader) - Async methods: End with
Asyncsuffix - Boolean properties: Prefer affirmative phrasing (e.g.,
IsAvailable,StayOpen)
- Nullable reference types are enabled globally - always handle potential nulls
- Use nullable annotations:
public string? ApiKey { get; set; } - Prefer non-nullable where possible:
public bool ShowAll { get; set; } = false; - Use default value initializers for properties:
public double WindowWidth { get; set; } = 420; - Implicit usings enabled - don't add redundant using statements
- Indentation: 4 spaces (no tabs)
- Braces: Allman style (opening brace on new line)
- Line length: Aim for ~120 characters, max 150
- Blank lines: One between methods, two between logical sections
- Object initializers: Preferred for new objects:
new ProviderUsage { ProviderId = "openai" } - String interpolation: Use
$"Value: {value}"over concatenation
- ArgumentException: For missing/invalid parameters with descriptive message
- Return error state in objects: For provider errors, return
ProviderUsagewithIsAvailable = false - Log exceptions: Use
_logger.LogError(ex, "message")for unexpected errors - Swallow specific exceptions: Only when appropriate with logging
- Never throw in async void: Use
Taskorasync Taskinstead
- Always
awaitasync calls (avoid.Resultor.Wait()) - Use
ConfigureAwait(false)in library code (non-UI) - Pass
CancellationTokenwhen available for long-running operations - Use
SemaphoreSlimfor async locking - Return
IEnumerablefor lazy evaluation,IListfor materialized collections
- Constructor injection only (no property injection)
- All dependencies declared as
readonlyfields - Register services in
App.xaml.cswithMicrosoft.Extensions.DependencyInjection - Use
ILogger<T>for logging (never useConsole.WriteLine)
- Arrange-Act-Assert pattern for all tests
- Use
[Fact]for normal tests,[Theory]with[InlineData]for parameterized tests - Mock interfaces with Moq:
var mock = new Mock<ILogger<ProviderManager>>(); - Use descriptive test names:
GetAllUsageAsync_LoadsConfigAndFetchesUsageFromMocks - Test both success and failure paths
- Avoid implementation details - test behavior
- Fixture Synchronization Required: Provider test/screenshot fixtures must be based on real provider responses (sanitized only), not invented values. Keep
docs/test_fixture_sync.mdand fixture data in sync in the same PR.
- XAML: 4-space indentation, self-closing tags when no content
- MVVM preferred: Keep code-behind minimal, use bindings
- Styles: Define in Window.Resources, use
x:Keyfor named styles - Colors: Use hex codes for dark theme (e.g.,
#1E1E1Ebackground) - Resource inclusion: Images as
<Resource>, SVG files as<Content>withPreserveNewest - Settings auth lookup: Slim UI must not spawn GitHub CLI (
gh) to resolve username; use local auth cache/files or provider API data.
The Agent is a background HTTP service that collects and stores provider usage data:
Key Components:
- UsageDatabase.cs: SQLite database with four tables (providers, provider_history, raw_snapshots, reset_events)
- ProviderRefreshService.cs: Scheduled refresh logic, filters providers without API keys
- ConfigService.cs: Configuration and preferences management
Port Management:
- Default port: 5000
- Auto-discovery: Tries ports 5000-5010, then random
- Port saved to:
%LOCALAPPDATA%\AIUsageTracker\monitor.json
Database Schema:
providers - Static provider configuration
provider_history - Time-series usage data (kept indefinitely)
raw_snapshots - Raw JSON data (14-day TTL, auto-cleanup)
reset_events - Quota/limit reset tracking (kept indefinitely)
The Web UI is an ASP.NET Core Razor Pages application that reads from the Monitor's database:
Features:
- Dashboard: Stats cards, provider usage with progress bars, 60s auto-refresh
- Providers: Table view of all providers with status
- Provider Details: Individual history + reset events
- History: Complete usage history across all providers
- Performance: Output caching, chart downsampling, deferred reset-events loading
Technology Stack:
- Framework: ASP.NET Core 8.0
- Pattern: Razor Pages (server-rendered)
- Styling: CSS variables for theming
- Database: Read-only access to Monitor's SQLite database
HTMX Integration:
- Auto-refresh via
hx-trigger="every 60s" - Partial page updates without full reload
- CDN loaded from unpkg.com
Web Performance Notes:
- Dashboard and Charts use short-lived output cache policies with query variance
- Hot DB reads use short-lived in-memory caches and structured timing/row-count logs
- Charts downsample server-side by time range and fetch reset events after initial render
Theme System: Seven built-in themes using CSS variables:
- Dark (default):
#1e1e1ebackground - Light: Clean white background
- High Contrast: Pure black/white for accessibility
- Solarized Dark: Blue-green palette
- Solarized Light: Sepia-toned
- Dracula: Purple/pink highlights
- Nord: Frosty blue-gray tones
Theme toggle in navbar with localStorage persistence.
The Slim UI reads the Monitor port directly from the configuration file:
Process:
- Read port from
%LOCALAPPDATA%\AIUsageTracker\monitor.json - Use that port for all API calls
Implementation:
MonitorService.RefreshPortAsync()- Reads port from monitor.jsonMonitorLauncher.GetAgentPortAsync()- Returns port from the file- NO port scanning - the file is the authoritative source
Always-On-Top Handling:
The Slim UI supports always-on-top mode via Topmost property and Win32 SetWindowPos API. When enabled, the window aggressively reasserts its z-order to prevent other applications from stealing focus.
Tooltip Coordination: When tooltips are visible, the aggressive z-order reassertion is temporarily disabled to prevent tooltips from being pushed behind the main window:
_isTooltipOpenflag tracks tooltip visibility stateReassertTopmostWithoutFocus()skips reassertion when_isTooltipOpenis trueEnsureAlwaysOnTop()also checks_isTooltipOpenbefore reasserting- Tooltips set their own
Topmost = truewhen opened
Implementation:
// Tooltip tracking in MainWindow.xaml.cs
toolTip.Opened += (s, e) => _isTooltipOpen = true;
toolTip.Closed += (s, e) => _isTooltipOpen = false;
// Guard in topmost reassertion methods
if (_isSettingsDialogOpen || _isTooltipOpen || !Topmost) return;Settings Dialog Coordination:
Similar to tooltips, the Settings dialog sets _isSettingsDialogOpen = true when shown to prevent the main window from fighting for z-order while a modal dialog is active.
CSP is configured in Program.cs with different policies for Development vs Production:
Development:
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://unpkg.com;- Allows HTMX eval and Browser Link/Hot Reload
Production:
script-src 'self' https://unpkg.com;- Strict policy - requires self-hosted HTMX
- No Essential Providers: There are no hardcoded "essential" providers that the application depends on.
- Key-Driven Activation: A provider is considered active and essential only if the user has provided a valid API key (either via configuration or environment variables).
- Listing: The UI pre-populates a list of supported providers to allow users to easily add keys, but their underlying presence is merely structural until configured.
- Equality: All supported providers are treated equally in terms of system integration and display logic.
When the Monitor starts, it MUST serve existing data from the database immediately and MUST NOT hammer 3rd party APIs.
Why: Users should see their providers immediately from cached data, but the Monitor should not overwhelm external APIs with startup requests. API calls should only happen on the scheduled refresh interval.
Implementation in ProviderRefreshService.cs:
// On startup with existing cached data:
// CORRECT: Serve from database, only refresh system providers (no external API)
_ = Task.Run(async () =>
{
await TriggerRefreshAsync(
forceAll: true,
includeProviderIds: new[] { "antigravity" });
});
// WRONG: Hammer all 3rd party APIs on startup
_ = Task.Run(async () =>
{
await TriggerRefreshAsync(forceAll: true); // DON'T DO THIS
});Key Principles:
- Serve from database immediately - The UI gets cached data right away
- No external API calls on startup - Only refresh system providers (antigravity)
- Scheduled refresh updates data - Normal 60s interval refreshes all providers
- Database is the source of truth - Between refreshes, serve what's stored
Consequence of wrong implementation: Hammering APIs on startup causes rate limiting and poor user experience.
NEVER delete customer data automatically. Do NOT implement cleanup logic that removes provider history.
Why: Customer data must be preserved. If placeholder data is being created, fix the SOURCE (don't write it) rather than cleaning it up later.
Implementation in UsageDatabase.cs:
// CORRECT: Filter placeholder data BEFORE storing
public async Task StoreHistoryAsync(IEnumerable<ProviderUsage> usages)
{
var validUsages = usages.Where(u =>
!(u.RequestsAvailable == 0 && u.RequestsUsed == 0 &&
u.RequestsPercentage == 0 && !u.IsAvailable &&
(u.Description?.Contains("API Key") == true ||
u.Description?.Contains("configured") == true))
).ToList();
// Only store validUsages...
}
// WRONG: Store everything then clean up later
public async Task StoreHistoryAsync(IEnumerable<ProviderUsage> usages)
{
// Store all usages including placeholders...
}
// Later...
await CleanupEmptyHistoryAsync(); // DON'T DO THIS - deletes customer dataKey Principles:
- Prevent placeholder data at the source - Filter in StoreHistoryAsync, not after
- NEVER delete provider history - Once stored, data must be preserved
- Raw snapshots can have TTL - CleanupOldSnapshotsAsync with 14-day retention is OK
- Customer data is sacred - Storage layer must protect it
Consequence of wrong implementation: Customer provider history is deleted, causing providers to disappear from the UI until next refresh.
NEVER use silent failures. Every exception, error, or unexpected condition MUST be logged.
Rule: All catch blocks must either:
- Re-throw the exception (if caller should handle it)
- Log the error with context
BAD:
catch
{
// Silent failure - user has no idea what went wrong
}GOOD:
catch (Exception ex)
{
_logger.LogDebug("GitHub CLI discovery failed: {Message}", ex.Message);
}Why this matters: Silent failures make debugging impossible. When users report "nothing works," we need logs to understand what failed.
public class ExampleProvider : IProviderService
{
public string ProviderId => "example";
private readonly HttpClient _httpClient;
private readonly ILogger<ExampleProvider> _logger;
public ExampleProvider(HttpClient httpClient, ILogger<ExampleProvider> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<IEnumerable<ProviderUsage>> GetUsageAsync(ProviderConfig config)
{
if (string.IsNullOrEmpty(config.ApiKey))
{
return new[] { new ProviderUsage
{
ProviderId = ProviderId,
ProviderName = "Example",
IsAvailable = false,
Description = "API Key missing"
}};
}
try
{
// Fetch usage...
return new[] { /* usage data */ };
}
catch (Exception ex)
{
_logger.LogError(ex, "Provider check failed");
return new[] { new ProviderUsage
{
ProviderId = ProviderId,
ProviderName = "Example",
IsAvailable = false,
Description = "Connection Failed"
}};
}
}
### Progress Bar Calculation for Payment Types
The application uses different progress bar calculations depending on the payment type to provide intuitive visual feedback. Additionally, users can toggle between showing "used" or "remaining" percentages.
#### Quota-Based Providers (e.g., Synthetic, Z.AI, GitHub Copilot)
**Calculation:** Show **remaining percentage** (full bar = lots of credits remaining)
```csharp
var utilization = (total - used) / total * 100.0;Visual Behavior:
- 0 used / 135 total = 100% full bar (all credits available used / 135 total = **50)
- 67% bar** (half credits remaining)
- 135 used / 135 total = 0% empty bar (no credits remaining)
Color Logic:
- Green: UsagePercentage > ColorThresholdYellow (lots remaining)
- Yellow: ColorThresholdRed < UsagePercentage <= ColorThresholdYellow (moderate remaining)
- Red: UsagePercentage < ColorThresholdRed (dangerously low remaining)
Rationale: Users expect to see a full green bar when they have all their quota available. The bar depletes and turns red as they use credits, similar to a fuel gauge.
Calculation: Show used percentage (full bar = high usage/spending)
var utilization = used / total * 100.0;Visual Behavior:
- 0 used / 100 total = 0% empty bar (no spending yet)
- 50 used / 100 total = 50% bar (moderate spending)
- 100 used / 100 total = 100% full bar (budget exhausted)
Color Logic:
- Green: UsagePercentage < ColorThresholdYellow (low spending)
- Yellow: ColorThresholdYellow <= UsagePercentage < ColorThresholdRed (moderate spending)
- Red: UsagePercentage >= ColorThresholdRed (high spending/budget exhausted)
Rationale: For pay-as-you-go providers, users want to see spending accumulate. The bar fills up and turns red as they spend money, acting as a spending warning indicator.
Users can toggle between viewing "used" or "remaining" percentages via the settings or UI toggle button. This affects only the display text, not the progress bar color.
Toggle Behavior:
- Toggle OFF (default): Shows "remaining" percentage (e.g., "75% remaining")
- Toggle ON: Shows "used" percentage (e.g., "25% used")
What the toggle changes:
- Display text: "X% remaining" ↔ "X% used"
- Status messages in the UI
What the toggle does NOT change:
- Progress bar color: Always based on used percentage regardless of toggle state
- The underlying data calculations
Implementation:
bool showUsed = ShowUsedToggle?.IsChecked ?? false;
// Display text changes based on toggle
var displayText = showUsed
? $"{usedPercent:F0}% used"
: $"{(100.0 - usedPercent):F0}% remaining";
// Color is ALWAYS based on used percentage
var barColor = GetProgressBarColor(usedPercent);Why colors stay consistent:
- Even when showing "remaining" in text, the bar color reflects actual usage
- This prevents confusion: green always means "healthy usage", red always means "high usage"
- Users can quickly assess their usage status regardless of which percentage they prefer to see
Backend (Provider):
// For quota-based providers, show remaining percentage (full bar = lots remaining)
// For other providers, show used percentage (full bar = high usage)
var utilization = paymentType == PaymentType.Quota
? (total > 0 ? ((total - used) / total) * 100.0 : 100) // Remaining % for quota
: (total > 0 ? (used / total) * 100.0 : 0); // Used % for othersFrontend (UI Color Logic):
// Color is ALWAYS based on used percentage, regardless of toggle state
double pctRemaining = isQuotaType ? usage.RequestsPercentage : Math.Max(0, 100 - usage.RequestsPercentage);
double pctUsed = isQuotaType ? Math.Max(0, 100 - usage.RequestsPercentage) : usage.RequestsPercentage;
// Toggle controls what user sees, NOT the color
bool showUsed = ShowUsedToggle?.IsChecked ?? false;
var displayText = showUsed ? $"{pctUsed:F0}% used" : $"{pctRemaining:F0}% remaining";
// Progress bar always uses pctUsed for color calculation
var barColor = GetProgressBarColor(pctUsed);Color Thresholds:
- Red: >= 90% used
- Yellow: >= 50% used
- Green: < 50% used
- Use
System.Text.Json(not Newtonsoft) - Configure with
JsonSerializerOptionsif needed - Prefer
await HttpClient.GetFromJsonAsync<T>()for GET requests - Use
JsonSerializer.Serialize()andJsonContent.Create()for POST
- SQLite via
Microsoft.Data.Sqlite - Encrypted storage using
System.Security.Cryptography.ProtectedData - Configuration stored in
auth.jsonin app data directory - Automatic backup created on updates
- Use
Microsoft.Extensions.Loggingwith structured logging - Log levels:
LogDebug,LogInformation,LogWarning,LogError - Include context in messages:
LogDebug($"Fetching usage for provider: {config.ProviderId}")
- Version numbers in
.csprojfiles:<Version>X.Y.Z</Version> - Update UI version for releases, other projects follow semantic versioning
- CI/CD triggered on tag push:
v* - Release Notes: Keep them concise. Do not repeat information already present in the change log. Focus on the high-level summary of changes.
- Changelog: Maintain a
CHANGELOG.mdfile with concise documentation of changes for each version. Include the date of the release. Also keep an## Unreleasedsection at the top for tracking upcoming changes.
See docs/release-process.md for the complete release process covering beta releases, stable releases, versioning, changelog, and appcast updates.
Key rules:
- All release-related changes MUST be made via pull request — never push directly to
mainordevelop. - Beta releases target
develop. Stable releases targetmain. - Version source of truth:
Directory.Build.props(<TrackerVersion>). - Appcast files are auto-generated by CI during the
publish.ymlworkflow.
Always add automated tests for UI startup sequences to prevent deadlocks and theme issues.
Example tests in AIUsageTracker.Tests/UI/AppStartupTests.cs:
[Fact]
public async Task LoadPreferencesAsync_DoesNotBlockThread()
{
// Ensures async loading doesn't block the UI thread
var startTime = DateTime.UtcNow;
var loadTask = UiPreferencesStore.LoadAsync();
var completed = await Task.WhenAny(loadTask, Task.Delay(TimeSpan.FromSeconds(5)));
Assert.Same(loadTask, completed);
Assert.True(DateTime.UtcNow - startTime < TimeSpan.FromSeconds(5),
"Loading preferences took too long - possible blocking call");
}
[Fact]
public async Task PreferencesStore_SaveLoad_NoDeadlock()
{
// Tests for deadlock in rapid save/load cycles
for (int i = 0; i < 10; i++)
{
await UiPreferencesStore.SaveAsync(preferences);
var loaded = await UiPreferencesStore.LoadAsync();
Assert.NotNull(loaded);
}
}What these tests catch:
- Synchronous
.GetAwaiter().GetResult()blocking calls - Deadlocks in preference save/load
- Theme revert issues
- Race conditions during startup
Run tests before committing:
dotnet test AIUsageTracker.Tests/AIUsageTracker.Tests.csproj --filter "FullyQualifiedName~AppStartupTests"- Test async behavior - Ensure async methods don't block
- Test defaults - Verify fallback values when files don't exist
- Test persistence - Round-trip save/load cycles
- Test edge cases - Null resources, corrupted files, rapid operations
- Test themes - Verify theme persistence across restarts
- GitHub Actions for testing on push/PR to main.
- Release workflow creates installers for multiple platforms.
- Winget submission for Windows packages.
Run Testsincludes a web endpoint perf smoke guardrail for/and/chartsin CI.
The Slim UI uses a dynamic polling strategy to ensure providers appear quickly while minimizing resource usage:
- Rapid Polling: Poll every 5 seconds until data is available
- Max Attempts: 15 attempts (75 seconds max)
- On No Data: Trigger background refresh and continue polling
- Display: Show cached data immediately, update when fresh data arrives
- Standard Interval: Poll every 1 minute
- Concurrent Prevention: Skip poll if previous still in progress
- Data Preservation: Never overwrite existing data with empty results
- Connection Error: Switch to rapid polling (5s)
- Monitor Unavailable: Show error but preserve cached data
- Refresh Failure: Keep last successful snapshot
- Always show something (cached data or loading state)
- Never block waiting for data
- Prefer stale data over empty UI
- Aggressive polling only during startup or errors
This algorithm ensures providers appear within 30 seconds while maintaining responsiveness.
See WPF Async/Await Best Practices for critical patterns to avoid UI blocking and deadlocks.
Key rules:
- Never use
.GetAwaiter().GetResult()or.Resulton the UI thread - Never create sync wrappers for async methods
- Always add timeouts to HTTP calls (> 30s is too long for UI)
- Use fire-and-forget (
_ =) for non-critical background work - Add exception handling to all
async voidevent handlers - Use
ConfigureAwait(false)in library code (Core/Infrastructure projects)