feat(ui): add usage monitor modal with gauges and model/task usage analysis
Adds a Usage Monitor modal (Worker menu + wired to the footer/Mission-Control usage pill's Open command): dynamic gauges built from UsageSnapshotDto.Limits with gate-threshold marks, a stale/blocked-gate band, and Models/Tasks tabs backed by GetModelUsageAsync/GetTaskUsageAsync over a 7d/30d/custom range.
This commit is contained in:
@@ -16,6 +16,7 @@ public interface IDialogService
|
||||
{
|
||||
Task ShowAboutAsync(AboutModalViewModel vm);
|
||||
Task ShowWeeklyReportAsync(WeeklyReportModalViewModel vm);
|
||||
Task ShowUsageMonitorAsync(UsageMonitorModalViewModel vm);
|
||||
Task ShowSettingsAsync(SettingsModalViewModel vm);
|
||||
Task ShowListSettingsAsync(ListSettingsModalViewModel vm);
|
||||
Task ShowRepoImportAsync(RepoImportModalViewModel vm);
|
||||
|
||||
@@ -37,6 +37,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext>? _dbFactory;
|
||||
private readonly Func<WorktreesOverviewModalViewModel> _worktreesOverviewVmFactory = () => null!;
|
||||
private readonly Func<WeeklyReportModalViewModel> _weeklyReportVmFactory = () => null!;
|
||||
private readonly Func<UsageMonitorModalViewModel> _usageMonitorVmFactory = () => null!;
|
||||
private readonly Func<MergeModalViewModel> _mergeVmFactory = () => null!;
|
||||
private readonly Func<RepoImportModalViewModel>? _repoImportVmFactory;
|
||||
|
||||
@@ -207,6 +208,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
||||
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||
Func<WorktreesOverviewModalViewModel> worktreesOverviewVmFactory,
|
||||
Func<WeeklyReportModalViewModel> weeklyReportVmFactory,
|
||||
Func<UsageMonitorModalViewModel> usageMonitorVmFactory,
|
||||
Func<MergeModalViewModel> mergeVmFactory,
|
||||
Func<RepoImportModalViewModel> repoImportVmFactory,
|
||||
MissionControlViewModel missionControl,
|
||||
@@ -215,6 +217,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
||||
Lists = lists; Tasks = tasks; Details = details; Worker = worker;
|
||||
MissionControl = missionControl;
|
||||
UsagePill = usagePill;
|
||||
UsagePill.OpenMonitorRequested += () => _ = OpenUsageMonitor();
|
||||
MissionControl.OpenInApp = id => _ = RevealTaskAsync(id);
|
||||
MissionControl.OpenSettingsRequested = () => Lists.OpenSettingsCommand.Execute(null);
|
||||
MissionControl.ErrorReported += FlashFooterError;
|
||||
@@ -226,6 +229,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
||||
_dbFactory = dbFactory;
|
||||
_worktreesOverviewVmFactory = worktreesOverviewVmFactory;
|
||||
_weeklyReportVmFactory = weeklyReportVmFactory;
|
||||
_usageMonitorVmFactory = usageMonitorVmFactory;
|
||||
_mergeVmFactory = mergeVmFactory;
|
||||
_repoImportVmFactory = repoImportVmFactory;
|
||||
Lists.SelectionChanged += (_, _) => Tasks.LoadForList(Lists.SelectedList);
|
||||
@@ -429,6 +433,23 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
||||
finally { _weeklyReportOpen = false; }
|
||||
}
|
||||
|
||||
private bool _usageMonitorOpen;
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenUsageMonitor()
|
||||
{
|
||||
if (Dialogs is null || _usageMonitorOpen) return;
|
||||
_usageMonitorOpen = true;
|
||||
try
|
||||
{
|
||||
var vm = _usageMonitorVmFactory();
|
||||
vm.ErrorReported += FlashFooterError;
|
||||
await vm.LoadAsync();
|
||||
await Dialogs.ShowUsageMonitorAsync(vm);
|
||||
}
|
||||
finally { _usageMonitorOpen = false; }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task CheckForUpdatesAsync()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels.Modals;
|
||||
|
||||
public sealed partial class UsageMonitorModalViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IWorkerClient _worker;
|
||||
private bool _applyingRange;
|
||||
|
||||
public UsageMonitorModalViewModel(IWorkerClient worker) => _worker = worker;
|
||||
|
||||
public Action? CloseAction { get; set; }
|
||||
public event Action<string>? ErrorReported;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(GaugeRows))]
|
||||
[NotifyPropertyChangedFor(nameof(IsStale))]
|
||||
[NotifyPropertyChangedFor(nameof(LastError))]
|
||||
[NotifyPropertyChangedFor(nameof(IsGateBlocked))]
|
||||
[NotifyPropertyChangedFor(nameof(GateReason))]
|
||||
[NotifyPropertyChangedFor(nameof(StaleStampText))]
|
||||
[NotifyPropertyChangedFor(nameof(StaleBandText))]
|
||||
[NotifyPropertyChangedFor(nameof(GateBandText))]
|
||||
private UsageSnapshotDto? _snapshot;
|
||||
|
||||
[ObservableProperty] private DateTime? _startDate;
|
||||
[ObservableProperty] private DateTime? _endDate;
|
||||
[ObservableProperty] private int _selectedPresetDays = 7;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ModelsEmpty))]
|
||||
private bool _isBusy;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ModelsEmpty))]
|
||||
private IReadOnlyList<ModelUsageDisplayRow> _modelRows = Array.Empty<ModelUsageDisplayRow>();
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(TasksEmpty))]
|
||||
private IReadOnlyList<TaskUsageDisplayRow> _taskRows = Array.Empty<TaskUsageDisplayRow>();
|
||||
|
||||
public bool ModelsEmpty => !IsBusy && ModelRows.Count == 0;
|
||||
public bool TasksEmpty => !IsBusy && TaskRows.Count == 0;
|
||||
|
||||
public IReadOnlyList<UsageGaugeRowViewModel> GaugeRows =>
|
||||
Snapshot is null ? Array.Empty<UsageGaugeRowViewModel>() : Snapshot.Limits.Select(BuildGaugeRow).ToList();
|
||||
|
||||
public bool IsStale => Snapshot?.IsStale == true;
|
||||
public string? LastError => Snapshot?.LastError;
|
||||
public bool IsGateBlocked => Snapshot?.IsGateBlocked == true;
|
||||
public string? GateReason => Snapshot?.GateReason;
|
||||
public string StaleStampText => Snapshot?.FetchedAtUtc is { } t ? t.ToLocalTime().ToString("HH:mm") : "?";
|
||||
public string StaleBandText => Loc.T("modals.usageMonitor.staleFormat", StaleStampText);
|
||||
public string GateBandText => GateReason is null ? "" : Loc.T("modals.usageMonitor.gateBlockedFormat", GateReason);
|
||||
|
||||
[RelayCommand]
|
||||
private void Close()
|
||||
{
|
||||
_worker.UsageUpdatedEvent -= OnUsageUpdated;
|
||||
CloseAction?.Invoke();
|
||||
}
|
||||
|
||||
public async Task LoadAsync()
|
||||
{
|
||||
Snapshot = await _worker.GetUsageSnapshotAsync();
|
||||
_worker.UsageUpdatedEvent -= OnUsageUpdated;
|
||||
_worker.UsageUpdatedEvent += OnUsageUpdated;
|
||||
ApplyPresetRange(SelectedPresetDays);
|
||||
await LoadUsageDataAsync();
|
||||
}
|
||||
|
||||
private void OnUsageUpdated(UsageSnapshotDto snapshot) => Snapshot = snapshot;
|
||||
|
||||
private void ApplyPresetRange(int days)
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
_applyingRange = true;
|
||||
SelectedPresetDays = days;
|
||||
StartDate = today.AddDays(-(days - 1)).ToDateTime(TimeOnly.MinValue);
|
||||
EndDate = today.ToDateTime(TimeOnly.MinValue);
|
||||
_applyingRange = false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private Task SetPreset(int days)
|
||||
{
|
||||
ApplyPresetRange(days);
|
||||
return LoadUsageDataAsync();
|
||||
}
|
||||
|
||||
partial void OnStartDateChanged(DateTime? value)
|
||||
{
|
||||
if (!_applyingRange) _ = LoadUsageDataAsync();
|
||||
}
|
||||
|
||||
partial void OnEndDateChanged(DateTime? value)
|
||||
{
|
||||
if (!_applyingRange) _ = LoadUsageDataAsync();
|
||||
}
|
||||
|
||||
private bool RangeValid => StartDate is not null && EndDate is not null && StartDate <= EndDate;
|
||||
|
||||
private async Task LoadUsageDataAsync()
|
||||
{
|
||||
if (!RangeValid) return;
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
var from = DateOnly.FromDateTime(StartDate!.Value);
|
||||
var to = DateOnly.FromDateTime(EndDate!.Value);
|
||||
var modelRows = await _worker.GetModelUsageAsync(from, to);
|
||||
var taskRows = await _worker.GetTaskUsageAsync(from, to);
|
||||
ModelRows = BuildModelDisplayRows(modelRows);
|
||||
TaskRows = taskRows
|
||||
.Select(r => new TaskUsageDisplayRow(r.TaskId, r.TaskTitle, r.ListName, r.Model, r.Runs, r.TokensIn, r.TokensOut))
|
||||
.OrderByDescending(r => r.TotalTokens)
|
||||
.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorReported?.Invoke(Loc.T("vm.usageMonitor.loadFailed", ex.Message));
|
||||
}
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
|
||||
private static string BuildGaugeLabel(UsageLimitDto limit) => limit.Kind switch
|
||||
{
|
||||
"session" => Loc.T("modals.usageMonitor.gaugeSession"),
|
||||
"weekly_all" => Loc.T("modals.usageMonitor.gaugeWeeklyAll"),
|
||||
"weekly_scoped" when !string.IsNullOrWhiteSpace(limit.ScopeModelDisplayName)
|
||||
=> Loc.T("modals.usageMonitor.gaugeWeeklyScopedFormat", limit.ScopeModelDisplayName!),
|
||||
_ => limit.Kind,
|
||||
};
|
||||
|
||||
private UsageGaugeRowViewModel BuildGaugeRow(UsageLimitDto limit)
|
||||
{
|
||||
int? threshold = limit.Kind switch
|
||||
{
|
||||
"session" => Snapshot?.FiveHourThresholdPct,
|
||||
"weekly_all" => Snapshot?.SevenDayThresholdPct,
|
||||
_ => null,
|
||||
};
|
||||
return new UsageGaugeRowViewModel(BuildGaugeLabel(limit), limit.Percent, limit.Severity, limit.ResetsAt, threshold);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ModelUsageDisplayRow> BuildModelDisplayRows(IReadOnlyList<ModelUsageRowDto> rows)
|
||||
{
|
||||
var built = new List<ModelUsageDisplayRow>();
|
||||
foreach (var group in rows.GroupBy(r => r.Model))
|
||||
{
|
||||
long cdIn = 0, cdOut = 0, cdCache = 0, otIn = 0, otOut = 0, otCache = 0;
|
||||
foreach (var row in group)
|
||||
{
|
||||
var cache = row.CacheReadTokens + row.CacheCreationTokens;
|
||||
if (string.Equals(row.Scope, "ClaudeDo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
cdIn += row.InputTokens; cdOut += row.OutputTokens; cdCache += cache;
|
||||
}
|
||||
else
|
||||
{
|
||||
otIn += row.InputTokens; otOut += row.OutputTokens; otCache += cache;
|
||||
}
|
||||
}
|
||||
built.Add(new ModelUsageDisplayRow(group.Key, cdIn, cdOut, cdCache, otIn, otOut, otCache));
|
||||
}
|
||||
|
||||
var grandTotal = built.Sum(r => r.GrandTotal);
|
||||
return built
|
||||
.Select(r => r with { SharePercent = grandTotal > 0 ? r.GrandTotal * 100.0 / grandTotal : 0 })
|
||||
.OrderByDescending(r => r.GrandTotal)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record UsageGaugeRowViewModel(
|
||||
string Label,
|
||||
double Percent,
|
||||
string Severity,
|
||||
DateTimeOffset? ResetsAt,
|
||||
int? ThresholdPercent)
|
||||
{
|
||||
public bool IsWarnSeverity => !string.Equals(Severity, "normal", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public string ResetText => ResetsAt is { } r ? Loc.T("modals.usageMonitor.resetIn", FormatRemaining(r)) : "";
|
||||
|
||||
// Matches the gauge card's inner track width in the view (240 card width - 12*2 padding).
|
||||
private const double GaugeTrackWidthPx = 216;
|
||||
|
||||
public double ThresholdMarkerLeftPx =>
|
||||
ThresholdPercent is { } t ? GaugeTrackWidthPx * Math.Clamp(t, 0, 100) / 100.0 : 0;
|
||||
|
||||
private static string FormatRemaining(DateTimeOffset resetsAt)
|
||||
{
|
||||
var remaining = resetsAt - DateTimeOffset.UtcNow;
|
||||
if (remaining < TimeSpan.Zero) remaining = TimeSpan.Zero;
|
||||
var hours = (int)remaining.TotalHours;
|
||||
var minutes = remaining.Minutes;
|
||||
return hours > 0
|
||||
? Loc.T("usage.pill.durationHoursMinutes", hours, minutes)
|
||||
: Loc.T("usage.pill.durationMinutes", minutes);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record ModelUsageDisplayRow(
|
||||
string Model,
|
||||
long ClaudeDoInputTokens,
|
||||
long ClaudeDoOutputTokens,
|
||||
long ClaudeDoCacheTokens,
|
||||
long OtherInputTokens,
|
||||
long OtherOutputTokens,
|
||||
long OtherCacheTokens)
|
||||
{
|
||||
public double SharePercent { get; init; }
|
||||
public long ClaudeDoTotal => ClaudeDoInputTokens + ClaudeDoOutputTokens + ClaudeDoCacheTokens;
|
||||
public long OtherTotal => OtherInputTokens + OtherOutputTokens + OtherCacheTokens;
|
||||
public long GrandTotal => ClaudeDoTotal + OtherTotal;
|
||||
}
|
||||
|
||||
public sealed record TaskUsageDisplayRow(
|
||||
string TaskId,
|
||||
string TaskTitle,
|
||||
string ListName,
|
||||
string? Model,
|
||||
int Runs,
|
||||
long TokensIn,
|
||||
long TokensOut)
|
||||
{
|
||||
public long TotalTokens => TokensIn + TokensOut;
|
||||
}
|
||||
@@ -66,6 +66,8 @@
|
||||
Command="{Binding RestartWorkerCommand}"/>
|
||||
<MenuItem Header="{loc:Tr shell.menu.checkForUpdates}"
|
||||
Command="{Binding CheckForUpdatesCommand}"/>
|
||||
<MenuItem Header="{loc:Tr shell.menu.usageMonitor}"
|
||||
Command="{Binding OpenUsageMonitorCommand}"/>
|
||||
</MenuItem>
|
||||
<MenuItem Header="{loc:Tr shell.menu.repositories}"
|
||||
FontSize="{StaticResource FontSizeMono}"
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
|
||||
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
|
||||
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
||||
xmlns:conv="using:Avalonia.Data.Converters"
|
||||
x:Class="ClaudeDo.Ui.Views.Modals.UsageMonitorModalView"
|
||||
x:DataType="vm:UsageMonitorModalViewModel"
|
||||
Title="{loc:Tr modals.usageMonitor.windowTitle}"
|
||||
Width="920" Height="680" MinWidth="700" MinHeight="480"
|
||||
CanResize="True"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Background="{DynamicResource SurfaceBrush}"
|
||||
WindowDecorations="BorderOnly"
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
ExtendClientAreaTitleBarHeightHint="-1">
|
||||
<Window.KeyBindings>
|
||||
<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>
|
||||
</Window.KeyBindings>
|
||||
|
||||
<Window.Styles>
|
||||
<Style Selector="ProgressBar.usage-gauge">
|
||||
<Setter Property="Height" Value="10"/>
|
||||
<Setter Property="CornerRadius" Value="4"/>
|
||||
<Setter Property="Minimum" Value="0"/>
|
||||
<Setter Property="Maximum" Value="100"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AccentBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="ProgressBar.usage-gauge.warn">
|
||||
<Setter Property="Foreground" Value="{DynamicResource StatusReviewBrush}"/>
|
||||
</Style>
|
||||
</Window.Styles>
|
||||
|
||||
<ctl:ModalShell Title="{loc:Tr modals.usageMonitor.title}" CloseCommand="{Binding CloseCommand}">
|
||||
<DockPanel>
|
||||
|
||||
<!-- Stale / gate bands -->
|
||||
<StackPanel DockPanel.Dock="Top" Spacing="4" Margin="20,16,20,0">
|
||||
<Border IsVisible="{Binding IsStale}"
|
||||
Background="{DynamicResource ReviewTintBrush}"
|
||||
BorderBrush="{DynamicResource StatusReviewBrush}"
|
||||
BorderThickness="1" CornerRadius="6" Padding="12,8">
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock Classes="meta" Text="{Binding StaleBandText}"/>
|
||||
<TextBlock Classes="meta" Text="{Binding LastError}" IsVisible="{Binding LastError, Converter={x:Static conv:StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<TextBlock Classes="meta" Text="{loc:Tr modals.usageMonitor.staleGateHint}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border IsVisible="{Binding IsGateBlocked}"
|
||||
Background="{DynamicResource ErrorTintBrush}"
|
||||
BorderBrush="{DynamicResource StatusErrorBrush}"
|
||||
BorderThickness="1" CornerRadius="6" Padding="12,8">
|
||||
<TextBlock Classes="meta" Text="{Binding GateBandText}"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Gauges -->
|
||||
<ItemsControl DockPanel.Dock="Top" Margin="20,12,20,0" ItemsSource="{Binding GaugeRows}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:UsageGaugeRowViewModel">
|
||||
<Border Background="{DynamicResource DeepBrush}" BorderBrush="{DynamicResource LineBrush}"
|
||||
BorderThickness="1" CornerRadius="6" Padding="12,10" Margin="0,0,10,10" Width="240">
|
||||
<StackPanel Spacing="6">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Classes="eyebrow" Text="{Binding Label}"/>
|
||||
<TextBlock Classes="meta" Text="{Binding Percent, StringFormat={}{0:0}%}" HorizontalAlignment="Right"/>
|
||||
</StackPanel>
|
||||
<Grid Height="10">
|
||||
<ProgressBar Classes="usage-gauge" Classes.warn="{Binding IsWarnSeverity}" Value="{Binding Percent}"/>
|
||||
<Canvas IsHitTestVisible="False">
|
||||
<Rectangle Canvas.Left="{Binding ThresholdMarkerLeftPx}" Width="2" Height="10"
|
||||
Fill="{DynamicResource TextDimBrush}"
|
||||
IsVisible="{Binding ThresholdPercent, Converter={x:Static conv:ObjectConverters.IsNotNull}}"/>
|
||||
</Canvas>
|
||||
</Grid>
|
||||
<TextBlock Classes="meta" Text="{Binding ResetText}" IsVisible="{Binding ResetText, Converter={x:Static conv:StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock DockPanel.Dock="Top" Margin="20,12,20,0" Classes="meta"
|
||||
Text="{loc:Tr modals.usageMonitor.noGauges}"
|
||||
IsVisible="{Binding !GaugeRows.Count}"/>
|
||||
|
||||
<!-- Period picker -->
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="20,12,20,0">
|
||||
<TextBlock Classes="meta" Text="{loc:Tr modals.usageMonitor.period}" VerticalAlignment="Center"/>
|
||||
<Button Classes="btn" Content="{loc:Tr modals.usageMonitor.last7Days}"
|
||||
Command="{Binding SetPresetCommand}" CommandParameter="7"/>
|
||||
<Button Classes="btn" Content="{loc:Tr modals.usageMonitor.last30Days}"
|
||||
Command="{Binding SetPresetCommand}" CommandParameter="30"/>
|
||||
<TextBlock Classes="meta" Text="{loc:Tr modals.usageMonitor.fromDate}" VerticalAlignment="Center"/>
|
||||
<ctl:ThemedDatePicker SelectedDate="{Binding StartDate}"/>
|
||||
<TextBlock Classes="meta" Text="{loc:Tr modals.usageMonitor.toDate}" VerticalAlignment="Center"/>
|
||||
<ctl:ThemedDatePicker SelectedDate="{Binding EndDate}"/>
|
||||
</StackPanel>
|
||||
|
||||
<TabControl Padding="20,12" TabStripPlacement="Top">
|
||||
|
||||
<TabItem Header="{loc:Tr modals.usageMonitor.modelUsage}">
|
||||
<ScrollViewer>
|
||||
<StackPanel>
|
||||
<TextBlock Classes="meta" Margin="0,16"
|
||||
Text="{loc:Tr modals.usageMonitor.emptyStateHint}"
|
||||
IsVisible="{Binding ModelsEmpty}"/>
|
||||
<Grid ColumnDefinitions="*,80,80,80,80,80,80,60" Margin="12,0,12,4" IsVisible="{Binding !ModelsEmpty}">
|
||||
<TextBlock Grid.Column="0" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnModel}"/>
|
||||
<TextBlock Grid.Column="1" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnIn}"/>
|
||||
<TextBlock Grid.Column="2" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnOut}"/>
|
||||
<TextBlock Grid.Column="3" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnCache}"/>
|
||||
<TextBlock Grid.Column="4" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnOtherIn}"/>
|
||||
<TextBlock Grid.Column="5" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnOtherOut}"/>
|
||||
<TextBlock Grid.Column="6" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnOtherCache}"/>
|
||||
<TextBlock Grid.Column="7" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnShare}"/>
|
||||
</Grid>
|
||||
<Border Height="1" Background="{DynamicResource LineBrush}" Margin="0,0,0,8" IsVisible="{Binding !ModelsEmpty}"/>
|
||||
<ItemsControl ItemsSource="{Binding ModelRows}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ModelUsageDisplayRow">
|
||||
<Grid ColumnDefinitions="*,80,80,80,80,80,80,60" Margin="12,4">
|
||||
<TextBlock Grid.Column="0" Classes="title" Text="{Binding Model}"/>
|
||||
<TextBlock Grid.Column="1" Classes="meta" Text="{Binding ClaudeDoInputTokens}"/>
|
||||
<TextBlock Grid.Column="2" Classes="meta" Text="{Binding ClaudeDoOutputTokens}"/>
|
||||
<TextBlock Grid.Column="3" Classes="meta" Text="{Binding ClaudeDoCacheTokens}"/>
|
||||
<TextBlock Grid.Column="4" Classes="meta" Text="{Binding OtherInputTokens}"/>
|
||||
<TextBlock Grid.Column="5" Classes="meta" Text="{Binding OtherOutputTokens}"/>
|
||||
<TextBlock Grid.Column="6" Classes="meta" Text="{Binding OtherCacheTokens}"/>
|
||||
<TextBlock Grid.Column="7" Classes="meta" Text="{Binding SharePercent, StringFormat={}{0:0}%}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="{loc:Tr modals.usageMonitor.taskUsage}">
|
||||
<ScrollViewer>
|
||||
<StackPanel>
|
||||
<TextBlock Classes="meta" Margin="0,16"
|
||||
Text="{loc:Tr modals.usageMonitor.emptyStateHint}"
|
||||
IsVisible="{Binding TasksEmpty}"/>
|
||||
<Grid ColumnDefinitions="*,110,90,60,80,80,80" Margin="12,0,12,4" IsVisible="{Binding !TasksEmpty}">
|
||||
<TextBlock Grid.Column="0" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnTask}"/>
|
||||
<TextBlock Grid.Column="1" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnList}"/>
|
||||
<TextBlock Grid.Column="2" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnModelShort}"/>
|
||||
<TextBlock Grid.Column="3" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnRuns}"/>
|
||||
<TextBlock Grid.Column="4" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnIn}"/>
|
||||
<TextBlock Grid.Column="5" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnOut}"/>
|
||||
<TextBlock Grid.Column="6" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnTotal}"/>
|
||||
</Grid>
|
||||
<Border Height="1" Background="{DynamicResource LineBrush}" Margin="0,0,0,8" IsVisible="{Binding !TasksEmpty}"/>
|
||||
<ItemsControl ItemsSource="{Binding TaskRows}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TaskUsageDisplayRow">
|
||||
<Grid ColumnDefinitions="*,110,90,60,80,80,80" Margin="12,4">
|
||||
<TextBlock Grid.Column="0" Classes="title" Text="{Binding TaskTitle}"/>
|
||||
<TextBlock Grid.Column="1" Classes="meta" Text="{Binding ListName}"/>
|
||||
<TextBlock Grid.Column="2" Classes="meta" Text="{Binding Model}"/>
|
||||
<TextBlock Grid.Column="3" Classes="meta" Text="{Binding Runs}"/>
|
||||
<TextBlock Grid.Column="4" Classes="meta" Text="{Binding TokensIn}"/>
|
||||
<TextBlock Grid.Column="5" Classes="meta" Text="{Binding TokensOut}"/>
|
||||
<TextBlock Grid.Column="6" Classes="meta" Text="{Binding TotalTokens}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
</TabControl>
|
||||
|
||||
<Ellipse Classes="spinner" Width="20" Height="20" Margin="0,0,0,16"
|
||||
HorizontalAlignment="Center" DockPanel.Dock="Bottom"
|
||||
IsVisible="{Binding IsBusy}"/>
|
||||
|
||||
</DockPanel>
|
||||
</ctl:ModalShell>
|
||||
</Window>
|
||||
@@ -0,0 +1,8 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace ClaudeDo.Ui.Views.Modals;
|
||||
|
||||
public partial class UsageMonitorModalView : Window
|
||||
{
|
||||
public UsageMonitorModalView() => InitializeComponent();
|
||||
}
|
||||
@@ -58,6 +58,13 @@ public sealed class WindowDialogService : IDialogService
|
||||
await dlg.ShowDialog(_owner);
|
||||
}
|
||||
|
||||
public async Task ShowUsageMonitorAsync(UsageMonitorModalViewModel vm)
|
||||
{
|
||||
var dlg = new UsageMonitorModalView { DataContext = vm };
|
||||
vm.CloseAction = () => dlg.Close();
|
||||
await dlg.ShowDialog(_owner);
|
||||
}
|
||||
|
||||
public async Task ShowSettingsAsync(SettingsModalViewModel vm)
|
||||
{
|
||||
var dlg = new SettingsModalView { DataContext = vm };
|
||||
|
||||
Reference in New Issue
Block a user