feat(ui): surface TokenTracker status and cost in the usage monitor
This commit is contained in:
@@ -44,15 +44,57 @@ public sealed partial class UsageMonitorModalViewModel : ViewModelBase
|
|||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyPropertyChangedFor(nameof(ModelsEmpty))]
|
[NotifyPropertyChangedFor(nameof(ModelsEmpty))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(TotalsText))]
|
||||||
private IReadOnlyList<ModelUsageDisplayRow> _modelRows = Array.Empty<ModelUsageDisplayRow>();
|
private IReadOnlyList<ModelUsageDisplayRow> _modelRows = Array.Empty<ModelUsageDisplayRow>();
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[NotifyPropertyChangedFor(nameof(TasksEmpty))]
|
[NotifyPropertyChangedFor(nameof(TasksEmpty))]
|
||||||
private IReadOnlyList<TaskUsageDisplayRow> _taskRows = Array.Empty<TaskUsageDisplayRow>();
|
private IReadOnlyList<TaskUsageDisplayRow> _taskRows = Array.Empty<TaskUsageDisplayRow>();
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(TokenTrackerMissing))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowInstallButton))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(NodeHintText))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(TokenTrackerErrorText))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(HasTokenTrackerError))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(AnalyticsStampText))]
|
||||||
|
private TokenTrackerStatusDto? _tokenTracker;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(TotalsText))]
|
||||||
|
private double _totalCostUsd;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowInstallButton))]
|
||||||
|
private bool _isInstalling;
|
||||||
|
|
||||||
public bool ModelsEmpty => !IsBusy && ModelRows.Count == 0;
|
public bool ModelsEmpty => !IsBusy && ModelRows.Count == 0;
|
||||||
public bool TasksEmpty => !IsBusy && TaskRows.Count == 0;
|
public bool TasksEmpty => !IsBusy && TaskRows.Count == 0;
|
||||||
|
|
||||||
|
public bool TokenTrackerMissing => TokenTracker is { Installed: false };
|
||||||
|
public bool ShowInstallButton => TokenTracker is { Installed: false, NodeOk: true } && !IsInstalling;
|
||||||
|
|
||||||
|
public string NodeHintText => TokenTracker switch
|
||||||
|
{
|
||||||
|
{ NodeOk: true } => "",
|
||||||
|
{ NodeVersion: { Length: > 0 } v } => Loc.T("modals.usageMonitor.ttNodeHintFormat", v),
|
||||||
|
_ => Loc.T("modals.usageMonitor.ttNodeMissing"),
|
||||||
|
};
|
||||||
|
|
||||||
|
public bool HasTokenTrackerError => TokenTracker?.LastError is { Length: > 0 };
|
||||||
|
public string TokenTrackerErrorText => TokenTracker?.LastError is { Length: > 0 } e
|
||||||
|
? Loc.T("modals.usageMonitor.ttErrorFormat", e)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
public string AnalyticsStampText => TokenTracker?.LastFetchedUtc is { } t
|
||||||
|
? Loc.T("modals.usageMonitor.analyticsStampFormat", t.ToLocalTime().ToString("HH:mm"))
|
||||||
|
: Loc.T("modals.usageMonitor.analyticsNever");
|
||||||
|
|
||||||
|
public string TotalsText => Loc.T(
|
||||||
|
"modals.usageMonitor.analyticsTotalsFormat",
|
||||||
|
ModelRows.Sum(r => r.GrandTotal).ToString("N0"),
|
||||||
|
TotalCostUsd.ToString("0.00"));
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private IReadOnlyList<UsageGaugeRowViewModel> _gaugeRows = Array.Empty<UsageGaugeRowViewModel>();
|
private IReadOnlyList<UsageGaugeRowViewModel> _gaugeRows = Array.Empty<UsageGaugeRowViewModel>();
|
||||||
|
|
||||||
@@ -154,6 +196,39 @@ public sealed partial class UsageMonitorModalViewModel : ViewModelBase
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private Task SetPreset30Days() => SetPreset(30);
|
private Task SetPreset30Days() => SetPreset(30);
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task RefreshAnalytics()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TokenTracker = await _worker.RefreshTokenTrackerAsync();
|
||||||
|
await LoadUsageDataAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ErrorReported?.Invoke(Loc.T("vm.usageMonitor.analyticsRefreshFailed", ex.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorReported is the channel the shell hangs FlashFooterError on, so even the "install
|
||||||
|
// running" notice goes through it rather than getting its own banner.
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task InstallTokenTracker()
|
||||||
|
{
|
||||||
|
IsInstalling = true;
|
||||||
|
ErrorReported?.Invoke(Loc.T("vm.usageMonitor.installStarted"));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TokenTracker = await _worker.InstallTokenTrackerAsync();
|
||||||
|
if (TokenTracker is { Installed: true }) await RefreshAnalytics();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ErrorReported?.Invoke(Loc.T("vm.usageMonitor.installFailed", ex.Message));
|
||||||
|
}
|
||||||
|
finally { IsInstalling = false; }
|
||||||
|
}
|
||||||
|
|
||||||
private Task SetPreset(int days)
|
private Task SetPreset(int days)
|
||||||
{
|
{
|
||||||
ApplyPresetRange(days);
|
ApplyPresetRange(days);
|
||||||
@@ -183,10 +258,14 @@ public sealed partial class UsageMonitorModalViewModel : ViewModelBase
|
|||||||
var modelRows = await _worker.GetModelUsageAsync(from, to);
|
var modelRows = await _worker.GetModelUsageAsync(from, to);
|
||||||
var taskRows = await _worker.GetTaskUsageAsync(from, to);
|
var taskRows = await _worker.GetTaskUsageAsync(from, to);
|
||||||
ModelRows = BuildModelDisplayRows(modelRows);
|
ModelRows = BuildModelDisplayRows(modelRows);
|
||||||
|
TotalCostUsd = modelRows.Sum(r => r.CostUsd ?? 0);
|
||||||
TaskRows = taskRows
|
TaskRows = taskRows
|
||||||
.Select(r => new TaskUsageDisplayRow(r.TaskId, r.TaskTitle, r.ListName, r.Model, r.Runs, r.TokensIn, r.TokensOut))
|
.Select(r => new TaskUsageDisplayRow(
|
||||||
|
r.TaskId, r.TaskTitle, r.ListName, r.Model, r.Runs, r.TokensIn, r.TokensOut,
|
||||||
|
r.CostUsd, r.Retries))
|
||||||
.OrderByDescending(r => r.TotalTokens)
|
.OrderByDescending(r => r.TotalTokens)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
TokenTracker = await _worker.GetTokenTrackerStatusAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -303,9 +382,11 @@ public sealed partial class UsageMonitorModalViewModel : ViewModelBase
|
|||||||
foreach (var group in rows.GroupBy(r => r.Model))
|
foreach (var group in rows.GroupBy(r => r.Model))
|
||||||
{
|
{
|
||||||
long cdIn = 0, cdOut = 0, cdCache = 0, otIn = 0, otOut = 0, otCache = 0;
|
long cdIn = 0, cdOut = 0, cdCache = 0, otIn = 0, otOut = 0, otCache = 0;
|
||||||
|
double cost = 0;
|
||||||
foreach (var row in group)
|
foreach (var row in group)
|
||||||
{
|
{
|
||||||
var cache = row.CacheReadTokens + row.CacheCreationTokens;
|
var cache = row.CacheReadTokens + row.CacheCreationTokens;
|
||||||
|
cost += row.CostUsd ?? 0;
|
||||||
if (string.Equals(row.Scope, "ClaudeDo", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(row.Scope, "ClaudeDo", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
cdIn += row.InputTokens; cdOut += row.OutputTokens; cdCache += cache;
|
cdIn += row.InputTokens; cdOut += row.OutputTokens; cdCache += cache;
|
||||||
@@ -315,7 +396,7 @@ public sealed partial class UsageMonitorModalViewModel : ViewModelBase
|
|||||||
otIn += row.InputTokens; otOut += row.OutputTokens; otCache += cache;
|
otIn += row.InputTokens; otOut += row.OutputTokens; otCache += cache;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
built.Add(new ModelUsageDisplayRow(group.Key, cdIn, cdOut, cdCache, otIn, otOut, otCache));
|
built.Add(new ModelUsageDisplayRow(group.Key, cdIn, cdOut, cdCache, otIn, otOut, otCache, cost));
|
||||||
}
|
}
|
||||||
|
|
||||||
var grandTotal = built.Sum(r => r.GrandTotal);
|
var grandTotal = built.Sum(r => r.GrandTotal);
|
||||||
@@ -496,12 +577,14 @@ public sealed record ModelUsageDisplayRow(
|
|||||||
long ClaudeDoCacheTokens,
|
long ClaudeDoCacheTokens,
|
||||||
long OtherInputTokens,
|
long OtherInputTokens,
|
||||||
long OtherOutputTokens,
|
long OtherOutputTokens,
|
||||||
long OtherCacheTokens)
|
long OtherCacheTokens,
|
||||||
|
double CostUsd)
|
||||||
{
|
{
|
||||||
public double SharePercent { get; init; }
|
public double SharePercent { get; init; }
|
||||||
public long ClaudeDoTotal => ClaudeDoInputTokens + ClaudeDoOutputTokens + ClaudeDoCacheTokens;
|
public long ClaudeDoTotal => ClaudeDoInputTokens + ClaudeDoOutputTokens + ClaudeDoCacheTokens;
|
||||||
public long OtherTotal => OtherInputTokens + OtherOutputTokens + OtherCacheTokens;
|
public long OtherTotal => OtherInputTokens + OtherOutputTokens + OtherCacheTokens;
|
||||||
public long GrandTotal => ClaudeDoTotal + OtherTotal;
|
public long GrandTotal => ClaudeDoTotal + OtherTotal;
|
||||||
|
public string CostText => CostUsd > 0 ? CostUsd.ToString("0.00") : "—";
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record TaskUsageDisplayRow(
|
public sealed record TaskUsageDisplayRow(
|
||||||
@@ -511,7 +594,11 @@ public sealed record TaskUsageDisplayRow(
|
|||||||
string? Model,
|
string? Model,
|
||||||
int Runs,
|
int Runs,
|
||||||
long TokensIn,
|
long TokensIn,
|
||||||
long TokensOut)
|
long TokensOut,
|
||||||
|
double? CostUsd,
|
||||||
|
int? Retries)
|
||||||
{
|
{
|
||||||
public long TotalTokens => TokensIn + TokensOut;
|
public long TotalTokens => TokensIn + TokensOut;
|
||||||
|
public string CostText => CostUsd is > 0 ? CostUsd.Value.ToString("0.00") : "—";
|
||||||
|
public string RetriesText => Retries?.ToString() ?? "—";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user