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:
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user