using System; using System.Collections.Generic; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; namespace ClaudeDo.Ui.ViewModels; /// Backs the footer/Mission-Control-header usage pill. One instance is shared by both hosts. public sealed partial class UsagePillViewModel : ViewModelBase { private readonly IWorkerClient _worker; [ObservableProperty] private UsageSnapshotDto? _snapshot; /// Host wires this to open the usage-monitor modal (not built yet — the command is a no-op until then). public event Action? OpenMonitorRequested; public UsagePillViewModel(IWorkerClient worker) { _worker = worker; _worker.UsageUpdatedEvent += OnUsageUpdated; _ = LoadAsync(); } private async System.Threading.Tasks.Task LoadAsync() { var snapshot = await _worker.GetUsageSnapshotAsync(); if (snapshot is not null) Snapshot = snapshot; } private void OnUsageUpdated(UsageSnapshotDto snapshot) => Snapshot = snapshot; partial void OnSnapshotChanged(UsageSnapshotDto? value) { OnPropertyChanged(nameof(Text)); OnPropertyChanged(nameof(IsWarn)); OnPropertyChanged(nameof(IsBlocked)); OnPropertyChanged(nameof(IsStale)); OnPropertyChanged(nameof(Tooltip)); OnPropertyChanged(nameof(ShowNormalDot)); OnPropertyChanged(nameof(ShowWarnDot)); OnPropertyChanged(nameof(ShowStaleDot)); } public string Text => BuildText(Snapshot); public bool IsBlocked => Snapshot?.IsGateBlocked == true; public bool IsStale => Snapshot?.IsStale == true; public bool IsWarn => Snapshot is { } s && ((s.FiveHourPercent is { } five && five >= s.FiveHourThresholdPct - 10) || (s.SevenDayPercent is { } seven && seven >= s.SevenDayThresholdPct - 10)); // Visual dot priority: blocked > stale > warn > normal — mirrors the connection pill's // mutually-exclusive dot Ellipses, since Blocked/Stale/Warn aren't mutually exclusive states. public bool ShowNormalDot => !IsBlocked && !IsStale && !IsWarn; public bool ShowWarnDot => IsWarn && !IsBlocked && !IsStale; public bool ShowStaleDot => IsStale && !IsBlocked; public string Tooltip => BuildTooltip(Snapshot); [RelayCommand] private void OpenMonitor() => OpenMonitorRequested?.Invoke(); private static string BuildText(UsageSnapshotDto? s) { if (s is null) return Loc.T("usage.pill.empty"); var five = s.FiveHourPercent; var seven = s.SevenDayPercent; if (five is not null && seven is not null) return Loc.T("usage.pill.bothFormat", FormatPct(five.Value), FormatPct(seven.Value)); if (five is not null) return Loc.T("usage.pill.fiveHourFormat", FormatPct(five.Value)); if (seven is not null) return Loc.T("usage.pill.sevenDayFormat", FormatPct(seven.Value)); return Loc.T("usage.pill.empty"); } private static string FormatPct(double v) => Math.Round(v).ToString("0"); private static string BuildTooltip(UsageSnapshotDto? s) { if (s is null) return Loc.T("usage.pill.empty"); var lines = new List(); if (s.FiveHourResetsAt is { } fiveReset) lines.Add(Loc.T("usage.pill.resetIn", Loc.T("usage.pill.fiveHourLabel"), FormatRemaining(fiveReset))); if (s.SevenDayResetsAt is { } sevenReset) lines.Add(Loc.T("usage.pill.resetIn", Loc.T("usage.pill.sevenDayLabel"), FormatRemaining(sevenReset))); if (s.IsGateBlocked && !string.IsNullOrEmpty(s.GateReason)) lines.Add(Loc.T("usage.pill.blockedReason", s.GateReason)); if (s.IsStale) { var stamp = s.FetchedAtUtc?.ToLocalTime().ToString("HH:mm") ?? "?"; lines.Add(Loc.T("usage.pill.stale", stamp)); if (!string.IsNullOrEmpty(s.LastError)) lines.Add(Loc.T("usage.pill.lastError", s.LastError)); } return lines.Count > 0 ? string.Join(Environment.NewLine, lines) : Loc.T("usage.pill.empty"); } 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); } }