Skip to content

Latest commit

 

History

History
760 lines (596 loc) · 30.9 KB

File metadata and controls

760 lines (596 loc) · 30.9 KB

AI Usage Tracker - Monitor Guidelines

This document provides essential information for agentic coding assistants working on this .NET 8.0 WPF application.

Critical Rules

NEVER Create Releases Without Explicit Permission

  • 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

Provider Visibility Requirement

  • 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

Lean Code Requirement

  • 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.

Database Architecture

  • 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.

Take Full Ownership

  • 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.

Development Workflow

  • Never push directly to main or develop: All changes MUST go through feature/fix branches with Pull Requests targeting develop. Beta releases target develop; only stable releases target main.
  • Never force push to main without 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. Use gh pr edit --base <branch> to change the target branch. Closing and recreating wastes CI runs and loses conversation context.

Verify Before Acting (CRITICAL)

Before implementing ANY change, verify the current state first:

  1. Before version bumps: Run git tag -l "v*" --sort=-v:refname | head -5 to check the latest released tag. Never trust file contents alone — tags may have advanced beyond what's in the files.
  2. Before branch operations: Run git log --oneline origin/<branch> -5 to see what's actually on the remote.
  3. Before releases: Check tags AND file versions AND changelog to ensure consistency.
  4. Before pushing: Run validation scripts locally:
    • bash scripts/validate-release-consistency.sh "<version>" for version bumps
    • dotnet build for compilation
    • dotnet test for test suite
  5. Before creating PRs: Check the correct target branch (develop for betas, main for stable).

Why: Pushing without verification wastes 5-10 minutes of CI time per failed round-trip and blocks the user.

Project Structure

  • 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 & Test Commands

Building

# 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

Testing

# 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"

Running the Monitor

# 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

Running the Web UI

# 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

Running the Slim UI

# 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 fails

Automated Screenshots

To generate updated screenshots for documentation (headless and in Privacy Mode):

# Run from the UI bin directory or project root
AIUsageTracker.exe --test --screenshot

Note

The --test flag enables explicit UI initialization required for headless rendering. This logic is gated to avoid performance overhead for normal users.

Screenshot Baseline Policy (Slim UI)

  • 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-screenshots artifact 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.

Pre-Commit Validation (CRITICAL)

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.sh

This script performs:

  1. Build Validation - Ensures solution compiles without errors
  2. Test Execution - Runs all unit tests (162 tests)
  3. Format Checking - Verifies code matches .editorconfig rules

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 warn

Do 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

Publishing

# Publish Windows UI
.\scripts\publish-app.ps1 -Runtime win-x64

# Publish Linux CLI
.\scripts\publish-app.ps1 -Runtime linux-x64

Code Style Guidelines

Imports & Namespaces

  • 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

Naming Conventions

  • 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 Async suffix
  • Boolean properties: Prefer affirmative phrasing (e.g., IsAvailable, StayOpen)

Type & Nullable Guidelines

  • 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

Formatting

  • 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

Error Handling

  • ArgumentException: For missing/invalid parameters with descriptive message
  • Return error state in objects: For provider errors, return ProviderUsage with IsAvailable = 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 Task or async Task instead

Async/Await Patterns

  • Always await async calls (avoid .Result or .Wait())
  • Use ConfigureAwait(false) in library code (non-UI)
  • Pass CancellationToken when available for long-running operations
  • Use SemaphoreSlim for async locking
  • Return IEnumerable for lazy evaluation, IList for materialized collections

Dependency Injection

  • Constructor injection only (no property injection)
  • All dependencies declared as readonly fields
  • Register services in App.xaml.cs with Microsoft.Extensions.DependencyInjection
  • Use ILogger<T> for logging (never use Console.WriteLine)

Testing Guidelines

  • 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.md and fixture data in sync in the same PR.

WPF-Specific Guidelines

  • 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:Key for named styles
  • Colors: Use hex codes for dark theme (e.g., #1E1E1E background)
  • Resource inclusion: Images as <Resource>, SVG files as <Content> with PreserveNewest
  • Settings auth lookup: Slim UI must not spawn GitHub CLI (gh) to resolve username; use local auth cache/files or provider API data.

Agent Architecture

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)

Web UI Architecture

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): #1e1e1e background
  • 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.

Slim UI Port Discovery

The Slim UI reads the Monitor port directly from the configuration file:

Process:

  1. Read port from %LOCALAPPDATA%\AIUsageTracker\monitor.json
  2. Use that port for all API calls

Implementation:

  • MonitorService.RefreshPortAsync() - Reads port from monitor.json
  • MonitorLauncher.GetAgentPortAsync() - Returns port from the file
  • NO port scanning - the file is the authoritative source

Slim UI Window Behavior

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:

  • _isTooltipOpen flag tracks tooltip visibility state
  • ReassertTopmostWithoutFocus() skips reassertion when _isTooltipOpen is true
  • EnsureAlwaysOnTop() also checks _isTooltipOpen before reasserting
  • Tooltips set their own Topmost = true when 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.

Content Security Policy

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

Provider Philosophy

  • 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.

Startup Refresh Behavior (CRITICAL)

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:

  1. Serve from database immediately - The UI gets cached data right away
  2. No external API calls on startup - Only refresh system providers (antigravity)
  3. Scheduled refresh updates data - Normal 60s interval refreshes all providers
  4. 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.

Data Preservation (CRITICAL)

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 data

Key Principles:

  1. Prevent placeholder data at the source - Filter in StoreHistoryAsync, not after
  2. NEVER delete provider history - Once stored, data must be preserved
  3. Raw snapshots can have TTL - CleanupOldSnapshotsAsync with 14-day retention is OK
  4. 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.

Error Handling and Logging (CRITICAL)

NEVER use silent failures. Every exception, error, or unexpected condition MUST be logged.

Rule: All catch blocks must either:

  1. Re-throw the exception (if caller should handle it)
  2. 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.

Provider Implementation Pattern

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.

Credits-Based Providers (e.g., OpenCode)

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.

User Toggle: Show Used vs Remaining

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

Implementation

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 others

Frontend (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

JSON Handling

  • Use System.Text.Json (not Newtonsoft)
  • Configure with JsonSerializerOptions if needed
  • Prefer await HttpClient.GetFromJsonAsync<T>() for GET requests
  • Use JsonSerializer.Serialize() and JsonContent.Create() for POST

Database & Storage

  • SQLite via Microsoft.Data.Sqlite
  • Encrypted storage using System.Security.Cryptography.ProtectedData
  • Configuration stored in auth.json in app data directory
  • Automatic backup created on updates

Logging

  • Use Microsoft.Extensions.Logging with structured logging
  • Log levels: LogDebug, LogInformation, LogWarning, LogError
  • Include context in messages: LogDebug($"Fetching usage for provider: {config.ProviderId}")

Versioning

  • Version numbers in .csproj files: <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.md file with concise documentation of changes for each version. Include the date of the release. Also keep an ## Unreleased section at the top for tracking upcoming changes.

Release Process

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 main or develop.
  • Beta releases target develop. Stable releases target main.
  • Version source of truth: Directory.Build.props (<TrackerVersion>).
  • Appcast files are auto-generated by CI during the publish.yml workflow.

Testing Guidelines

UI Startup Tests (CRITICAL)

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"

Testing Principles

  1. Test async behavior - Ensure async methods don't block
  2. Test defaults - Verify fallback values when files don't exist
  3. Test persistence - Round-trip save/load cycles
  4. Test edge cases - Null resources, corrupted files, rapid operations
  5. Test themes - Verify theme persistence across restarts

CI/CD

  • GitHub Actions for testing on push/PR to main.
  • Release workflow creates installers for multiple platforms.
  • Winget submission for Windows packages.
  • Run Tests includes a web endpoint perf smoke guardrail for / and /charts in CI.

UI Polling Algorithm

The Slim UI uses a dynamic polling strategy to ensure providers appear quickly while minimizing resource usage:

Startup Phase

  1. Rapid Polling: Poll every 5 seconds until data is available
  2. Max Attempts: 15 attempts (75 seconds max)
  3. On No Data: Trigger background refresh and continue polling
  4. Display: Show cached data immediately, update when fresh data arrives

Normal Operation

  1. Standard Interval: Poll every 1 minute
  2. Concurrent Prevention: Skip poll if previous still in progress
  3. Data Preservation: Never overwrite existing data with empty results

Error Handling

  • Connection Error: Switch to rapid polling (5s)
  • Monitor Unavailable: Show error but preserve cached data
  • Refresh Failure: Keep last successful snapshot

Key Principles

  • 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.

Async/Await Best Practices

See WPF Async/Await Best Practices for critical patterns to avoid UI blocking and deadlocks.

Key rules:

  • Never use .GetAwaiter().GetResult() or .Result on 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 void event handlers
  • Use ConfigureAwait(false) in library code (Core/Infrastructure projects)