From f5b27f46afc68c4e641f28870117e620172cdc74 Mon Sep 17 00:00:00 2001 From: Gaoyang Date: Thu, 6 Aug 2026 09:48:56 +0800 Subject: [PATCH 1/2] docs: note test project structure and DataService progress reporting --- CLAUDE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2c11aae..95358d8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,6 +12,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - Run WPF app (Windows-only): `dotnet run --project src/AIUsageMonitor.WPF` - Run tests: `dotnet test AIUsageMonitor.slnx` - Single test: `dotnet test tests/AIUsageMonitor.Core.Tests --filter "FullyQualifiedName~MethodName"` + - Tests live only under `tests/AIUsageMonitor.Core.Tests`, mirroring Core's `Analytics/` and `Providers/Claude/` folders — there are no Cli or WPF test projects. - Versioning is via MinVer, driven by `v*` git tags (prefix `v`); no manual version bumps in project files. ## Architecture @@ -30,7 +31,7 @@ Provider-specific code lives under `Providers//` and implements `Providers `Analytics/UsageAnalyzer` computes daily/period/model-distribution/hourly/session summaries from an `IUsageProvider`'s `StatsCache`, using `Analytics/CostCalculator` for token cost estimation → `Services/DataService` is the single facade over all of this, consumed by both Cli and WPF. `StatsCache` is currently Claude's own cache-file shape (`Providers/Claude/Models`); adding a second provider will require either normalizing its output to that shape or generalizing `UsageAnalyzer`'s input type. -`DataService` caches the parsed `StatsCache` for 30 seconds and invalidates early via a `FileSystemWatcher` on `stats-cache.json` (Claude-provider-specific, via a type check in `DataService`'s constructor). Session-level summaries (`GetSessionSummaries`) are read fresh from the raw session files rather than the cache. +`DataService` caches the parsed `StatsCache` for 30 seconds and invalidates early via a `FileSystemWatcher` on `stats-cache.json` (Claude-provider-specific, via a type check in `DataService`'s constructor). Session-level summaries (`GetSessionSummaries`) are read fresh from the raw session files rather than the cache. Long-running `DataService` reads accept an optional `IProgress`, which the Cli surfaces as a Spectre.Console progress bar. DI is wired through `ServiceCollectionExtensions.AddClaudeUsageCore()`, which registers the Claude provider's locator/parsers, binds it as the singleton `IUsageProvider`, and registers `CostCalculator`, `UsageAnalyzer`, and `DataService`. From f41576e77f183968abf9428d470e8192750b8ab3 Mon Sep 17 00:00:00 2001 From: Gaoyang Date: Thu, 6 Aug 2026 09:49:03 +0800 Subject: [PATCH 2/2] fix: show zero-valued today summary and align hourly chart to midnight today/watch's daily summary previously printed "No data for today" instead of a table when no activity was recorded yet; it now renders a zero-valued DailySummary via the new DailySummary.Empty factory. The tokens-by-hour chart used a trailing N-hour window (--recent-hours, default 6) that could cross into the previous day. It now always spans from local midnight to now, so --recent-hours was removed as unused. --- .../Commands/TodayCommand.cs | 20 +++++-------------- .../Commands/WatchCommand.cs | 20 +++++++------------ .../Models/AnalyticsDtos.cs | 10 +++++++++- 3 files changed, 21 insertions(+), 29 deletions(-) diff --git a/src/AIUsageMonitor.Cli/Commands/TodayCommand.cs b/src/AIUsageMonitor.Cli/Commands/TodayCommand.cs index e14c7d0..a620cc4 100644 --- a/src/AIUsageMonitor.Cli/Commands/TodayCommand.cs +++ b/src/AIUsageMonitor.Cli/Commands/TodayCommand.cs @@ -1,4 +1,5 @@ using AIUsageMonitor.Cli.Rendering; +using AIUsageMonitor.Core.Models; using AIUsageMonitor.Core.Services; using Spectre.Console; using System.CommandLine; @@ -18,26 +19,15 @@ public static class TodayCommand public static Command Create(DataService dataService) { var command = new Command("today", "Show today's usage summary"); - var recentHoursOption = new Option("--recent-hours") - { - Description = "Trailing window (in hours) for the recent activity block", - DefaultValueFactory = _ => 6 - }; - command.Options.Add(recentHoursOption); - command.SetAction(parseResult => + command.SetAction(_ => { var summary = ProgressReporter.Run("Loading usage data...", - p => dataService.GetDailySummary(DateOnly.FromDateTime(DateTime.Today), p)); - if (summary is null) - { - AnsiConsole.MarkupLine("[yellow]No data for today.[/]"); - return 0; - } + p => dataService.GetDailySummary(DateOnly.FromDateTime(DateTime.Today), p)) + ?? DailySummary.Empty(DateOnly.FromDateTime(DateTime.Today)); - var recentHours = Math.Max(1, parseResult.GetValue(recentHoursOption)); var recent = ProgressReporter.Run("Loading recent activity...", - p => dataService.GetRecentActivity(TimeSpan.FromHours(recentHours), p)); + p => dataService.GetRecentActivity(DateTimeOffset.Now - DateTimeOffset.Now.Date, p)); AnsiConsole.Write(new Rows( SpectreRenderer.BuildDailySummary(summary), diff --git a/src/AIUsageMonitor.Cli/Commands/WatchCommand.cs b/src/AIUsageMonitor.Cli/Commands/WatchCommand.cs index a5d2719..7ed7f85 100644 --- a/src/AIUsageMonitor.Cli/Commands/WatchCommand.cs +++ b/src/AIUsageMonitor.Cli/Commands/WatchCommand.cs @@ -1,4 +1,5 @@ using AIUsageMonitor.Cli.Rendering; +using AIUsageMonitor.Core.Models; using AIUsageMonitor.Core.Services; using Spectre.Console; using Spectre.Console.Rendering; @@ -29,29 +30,22 @@ public static Command Create(DataService dataService) Description = "Refresh interval in seconds", DefaultValueFactory = _ => 2 }; - var recentHoursOption = new Option("--recent-hours") - { - Description = "Trailing window (in hours) for the hourly token chart shown in the today view", - DefaultValueFactory = _ => 6 - }; command.Options.Add(viewOption); command.Options.Add(intervalOption); - command.Options.Add(recentHoursOption); command.SetAction(async (parseResult, ct) => { var view = parseResult.GetValue(viewOption)!; var interval = Math.Max(1, parseResult.GetValue(intervalOption)); - var recentHours = Math.Max(1, parseResult.GetValue(recentHoursOption)); IRenderable BuildCurrent(IProgress? progress = null) => view switch { - "today" => dataService.GetDailySummary(DateOnly.FromDateTime(DateTime.Today), progress) is { } d - ? new Rows( - SpectreRenderer.BuildDailySummary(d), - new Rule().RuleStyle("grey"), - SpectreRenderer.BuildHourlyTokenChart(dataService.GetRecentActivity(TimeSpan.FromHours(recentHours), progress).HourlyTrend)) - : new Markup("[yellow]No data for today.[/]"), + "today" => new Rows( + SpectreRenderer.BuildDailySummary( + dataService.GetDailySummary(DateOnly.FromDateTime(DateTime.Today), progress) + ?? DailySummary.Empty(DateOnly.FromDateTime(DateTime.Today))), + new Rule().RuleStyle("grey"), + SpectreRenderer.BuildHourlyTokenChart(dataService.GetRecentActivity(DateTimeOffset.Now - DateTimeOffset.Now.Date, progress).HourlyTrend)), "week" => SpectreRenderer.BuildPeriodSummary( dataService.GetPeriodSummary(DateOnly.FromDateTime(DateTime.Today).AddDays(-6), DateOnly.FromDateTime(DateTime.Today), progress)), "models" => SpectreRenderer.BuildModelDistribution(dataService.GetModelDistribution(progress)), diff --git a/src/AIUsageMonitor.Core/Models/AnalyticsDtos.cs b/src/AIUsageMonitor.Core/Models/AnalyticsDtos.cs index f77d208..480c439 100644 --- a/src/AIUsageMonitor.Core/Models/AnalyticsDtos.cs +++ b/src/AIUsageMonitor.Core/Models/AnalyticsDtos.cs @@ -19,7 +19,15 @@ public sealed record DailySummary( int ToolCalls, long TotalTokens, Dictionary TokensByModel, - decimal EstimatedCost); + decimal EstimatedCost) +{ + /// + /// Creates a zero-valued for a date with no recorded activity. + /// + /// The calendar date the summary covers. + /// A with all counters at zero. + public static DailySummary Empty(DateOnly date) => new(date, 0, 0, 0, 0, [], 0m); +} /// /// Represents an aggregated summary of usage activity over a date range.