The modal deliberately shows only a slice of the analytics; this hands off to TokenTracker's own local dashboard for the rest. The worker starts `tokentracker serve` on demand and returns the URL, the UI opens the browser. Three things the spawn has to get right: port 7680 is not free on Windows (Delivery Optimization holds [::]:7680) and serve does not fall back, so we scan 7680-7689 with a dual-stack bind probe; --no-open because the CLI would open the browser before the server answers; and the child is a cmd.exe shim, so shutdown kills the process tree. --no-sync keeps our no-cloud-sync rule.
280 lines
18 KiB
XML
280 lines
18 KiB
XML
<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>
|
|
|
|
<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>
|
|
<Border IsVisible="{Binding IsThrottled}"
|
|
Background="{DynamicResource ReviewTintBrush}"
|
|
BorderBrush="{DynamicResource StatusReviewBrush}"
|
|
BorderThickness="1" CornerRadius="6" Padding="12,8">
|
|
<TextBlock Classes="meta" Text="{Binding ThrottleBandText}"/>
|
|
</Border>
|
|
</StackPanel>
|
|
|
|
<!-- Refresh: the worker polls on a slow cadence to avoid the endpoint's 429s -->
|
|
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8"
|
|
Margin="20,12,20,0" VerticalAlignment="Center">
|
|
<Button Classes="btn" Content="{loc:Tr modals.usageMonitor.refresh}"
|
|
Command="{Binding RefreshCommand}"
|
|
IsEnabled="{Binding !IsRefreshing}"/>
|
|
<Ellipse Classes="spinner" Width="14" Height="14" VerticalAlignment="Center"
|
|
IsVisible="{Binding IsRefreshing}"/>
|
|
<TextBlock Classes="meta" VerticalAlignment="Center"
|
|
Text="{loc:Tr modals.usageMonitor.refreshHint}"/>
|
|
</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="270">
|
|
<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>
|
|
<ctl:UsageGaugeBar Height="16"
|
|
Percent="{Binding Percent}"
|
|
IsWarn="{Binding IsWarnSeverity}"
|
|
SoftPct="{Binding SoftPct, Mode=TwoWay}"
|
|
HardPct="{Binding HardPct, Mode=TwoWay}"
|
|
GatePct="{Binding GatePct, Mode=TwoWay}"
|
|
CommitCommand="{Binding CommitCommand}"
|
|
TrackBrush="{DynamicResource LineBrush}"
|
|
FillBrush="{DynamicResource AccentBrush}"
|
|
WarnFillBrush="{DynamicResource StatusReviewBrush}"
|
|
SoftMarkerBrush="{DynamicResource TextDimBrush}"
|
|
HardMarkerBrush="{DynamicResource StatusReviewBrush}"
|
|
GateMarkerBrush="{DynamicResource StatusErrorBrush}"
|
|
ToolTip.Tip="{loc:Tr modals.usageMonitor.dragHint}"/>
|
|
|
|
<!-- Legend doubles as the numeric editor: swatch colours match the bar's markers,
|
|
and each box commits on Enter / focus loss (handlers in the code-behind). -->
|
|
<Grid ColumnDefinitions="10,*,62" RowDefinitions="Auto,Auto,Auto"
|
|
IsVisible="{Binding IsAdjustable}" Margin="0,2,0,0">
|
|
<Rectangle Grid.Row="0" Grid.Column="0" Width="10" Height="3" RadiusX="1.5" RadiusY="1.5"
|
|
VerticalAlignment="Center" Fill="{DynamicResource TextDimBrush}"/>
|
|
<TextBlock Grid.Row="0" Grid.Column="1" Classes="meta" Margin="6,0,4,0"
|
|
VerticalAlignment="Center" Text="{loc:Tr modals.usageMonitor.legendSoft}"/>
|
|
<NumericUpDown Grid.Row="0" Grid.Column="2" Tag="soft"
|
|
Value="{Binding SoftPct, Mode=TwoWay, Converter={StaticResource KeepLastNumber}}"
|
|
Minimum="0" Maximum="100" Increment="5" FormatString="0"
|
|
ShowButtonSpinner="False" FontSize="12" Padding="6,2"
|
|
LostFocus="OnStageBoxCommit" KeyDown="OnStageBoxKeyDown"/>
|
|
|
|
<Rectangle Grid.Row="1" Grid.Column="0" Width="10" Height="3" RadiusX="1.5" RadiusY="1.5"
|
|
VerticalAlignment="Center" Fill="{DynamicResource StatusReviewBrush}"/>
|
|
<TextBlock Grid.Row="1" Grid.Column="1" Classes="meta" Margin="6,0,4,0"
|
|
VerticalAlignment="Center" Text="{loc:Tr modals.usageMonitor.legendHard}"/>
|
|
<NumericUpDown Grid.Row="1" Grid.Column="2" Tag="hard"
|
|
Value="{Binding HardPct, Mode=TwoWay, Converter={StaticResource KeepLastNumber}}"
|
|
Minimum="0" Maximum="100" Increment="5" FormatString="0"
|
|
ShowButtonSpinner="False" FontSize="12" Padding="6,2"
|
|
LostFocus="OnStageBoxCommit" KeyDown="OnStageBoxKeyDown"/>
|
|
|
|
<Rectangle Grid.Row="2" Grid.Column="0" Width="10" Height="3" RadiusX="1.5" RadiusY="1.5"
|
|
VerticalAlignment="Center" Fill="{DynamicResource StatusErrorBrush}"/>
|
|
<TextBlock Grid.Row="2" Grid.Column="1" Classes="meta" Margin="6,0,4,0"
|
|
VerticalAlignment="Center" Text="{loc:Tr modals.usageMonitor.legendGate}"/>
|
|
<NumericUpDown Grid.Row="2" Grid.Column="2" Tag="gate"
|
|
Value="{Binding GatePct, Mode=TwoWay, Converter={StaticResource KeepLastNumber}}"
|
|
Minimum="0" Maximum="100" Increment="5" FormatString="0"
|
|
ShowButtonSpinner="False" FontSize="12" Padding="6,2"
|
|
LostFocus="OnStageBoxCommit" KeyDown="OnStageBoxKeyDown"/>
|
|
</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 SetPreset7DaysCommand}"/>
|
|
<Button Classes="btn" Content="{loc:Tr modals.usageMonitor.last30Days}"
|
|
Command="{Binding SetPreset30DaysCommand}"/>
|
|
<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>
|
|
|
|
<!-- TokenTracker hint: shown instead of the analytics header while the CLI is missing -->
|
|
<Border DockPanel.Dock="Top" Margin="20,12,20,0"
|
|
IsVisible="{Binding TokenTrackerMissing}"
|
|
Background="{DynamicResource DeepBrush}"
|
|
BorderBrush="{DynamicResource StatusReviewBrush}"
|
|
BorderThickness="1" CornerRadius="6" Padding="12,10">
|
|
<StackPanel Spacing="6">
|
|
<TextBlock Classes="title" Text="{loc:Tr modals.usageMonitor.ttMissingTitle}"/>
|
|
<TextBlock Classes="meta" TextWrapping="Wrap"
|
|
Text="{loc:Tr modals.usageMonitor.ttMissingBody}"/>
|
|
<TextBlock Classes="meta" TextWrapping="Wrap"
|
|
IsVisible="{Binding !ShowInstallButton}"
|
|
Text="{Binding NodeHintText}"/>
|
|
<TextBlock Classes="meta" TextWrapping="Wrap"
|
|
IsVisible="{Binding HasTokenTrackerError}"
|
|
Text="{Binding TokenTrackerErrorText}"/>
|
|
<Button Classes="btn" HorizontalAlignment="Left"
|
|
IsVisible="{Binding ShowInstallButton}"
|
|
Content="{loc:Tr modals.usageMonitor.ttInstall}"
|
|
Command="{Binding InstallTokenTrackerCommand}"/>
|
|
<Ellipse Classes="spinner" Width="14" Height="14" HorizontalAlignment="Left"
|
|
IsVisible="{Binding IsInstalling}"/>
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!-- Analytics header: totals, freshness stamp, manual refresh -->
|
|
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="12"
|
|
Margin="20,12,20,0" VerticalAlignment="Center"
|
|
IsVisible="{Binding !TokenTrackerMissing}">
|
|
<TextBlock Classes="title" VerticalAlignment="Center" Text="{Binding TotalsText}"/>
|
|
<TextBlock Classes="meta" VerticalAlignment="Center" Text="{Binding AnalyticsStampText}"/>
|
|
<Button Classes="btn" Content="{loc:Tr modals.usageMonitor.analyticsRefresh}"
|
|
Command="{Binding RefreshAnalyticsCommand}"/>
|
|
<Button Classes="btn" Content="{loc:Tr modals.usageMonitor.openDashboard}"
|
|
ToolTip.Tip="{loc:Tr modals.usageMonitor.openDashboardTip}"
|
|
IsEnabled="{Binding !IsOpeningDashboard}"
|
|
Command="{Binding OpenDashboardCommand}"/>
|
|
<Ellipse Classes="spinner" Width="14" Height="14" VerticalAlignment="Center"
|
|
IsVisible="{Binding IsOpeningDashboard}"/>
|
|
</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,80" 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}"/>
|
|
<TextBlock Grid.Column="8" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnCost}"/>
|
|
</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,80" 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}%}"/>
|
|
<TextBlock Grid.Column="8" Classes="meta" Text="{Binding CostText}"/>
|
|
</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,80,70" 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}"/>
|
|
<TextBlock Grid.Column="7" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnCost}"/>
|
|
<TextBlock Grid.Column="8" Classes="eyebrow" Text="{loc:Tr modals.usageMonitor.columnRetries}"/>
|
|
</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,80,70" 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}"/>
|
|
<TextBlock Grid.Column="7" Classes="meta" Text="{Binding CostText}"/>
|
|
<TextBlock Grid.Column="8" Classes="meta" Text="{Binding RetriesText}"/>
|
|
</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>
|