using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Input.Platform; using ClaudeDo.Data.Models; using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace ClaudeDo.Ui.ViewModels.Modals; /// /// Log Visualizer overlay — shows the worker's last 30 min of log records (all levels), /// fetched once on open via with a manual /// Refresh and a "warnings & errors only" filter. /// public sealed partial class LogVisualizerViewModel : ViewModelBase { private const int CopyLastCount = 40; private readonly IWorkerClient _worker; private readonly Func> _copyToClipboard; private IReadOnlyList _all = Array.Empty(); public ObservableCollection Rows { get; } = new(); [ObservableProperty] private bool _warnErrorOnly; [ObservableProperty] private string _statusText = ""; public Action? CloseAction { get; set; } public LogVisualizerViewModel(IWorkerClient worker) : this(worker, CopyToClipboardAsync) { } internal LogVisualizerViewModel(IWorkerClient worker, Func> copyToClipboard) { _worker = worker; _copyToClipboard = copyToClipboard; Rows.CollectionChanged += (_, _) => CopyLastCommand.NotifyCanExecuteChanged(); // Phase 3 reconcile tick: this overlay is long-lived (stays open while the user reads // through the log), so refresh it on the same cadence as TasksIslandViewModel's tick // instead of leaving it frozen at the moment it was opened. _reconcileTimer.Elapsed += (_, _) => Avalonia.Threading.Dispatcher.UIThread.Post(() => _ = RefreshAsync()); _reconcileTimer.Start(); } private readonly System.Timers.Timer _reconcileTimer = new(4_000); [RelayCommand] public async Task RefreshAsync() { _all = await _worker.GetRecentLogsAsync(); Apply(); } partial void OnWarnErrorOnlyChanged(bool value) => Apply(); private void Apply() { Rows.Clear(); IEnumerable items = WarnErrorOnly ? _all.Where(e => e.Level is WorkerLogLevel.Warn or WorkerLogLevel.Error) : _all; foreach (var e in items.Reverse()) Rows.Add(new LogVisualizerRow(e.TimestampUtc.ToLocalTime().ToString("HH:mm:ss"), e.Message, e.Level)); StatusText = Rows.Count == 0 ? Loc.T("modals.logVisualizer.empty") : Loc.T("modals.logVisualizer.count", Rows.Count); } [RelayCommand] private void Close() { _reconcileTimer.Stop(); _reconcileTimer.Dispose(); CloseAction?.Invoke(); } private bool CanCopyLast() => Rows.Count > 0; [RelayCommand(CanExecute = nameof(CanCopyLast))] private async Task CopyLastAsync() { var count = Math.Min(Rows.Count, CopyLastCount); var ok = await _copyToClipboard(BuildCopyText()); StatusText = ok ? Loc.T("modals.logVisualizer.copied", count) : Loc.T("modals.logVisualizer.copyFailed"); } internal string BuildCopyText() => string.Join('\n', Rows.Take(CopyLastCount).Reverse() .Select(r => $"{r.Time} {r.Level.ToString().ToUpperInvariant()} {r.Message}")); private static async Task CopyToClipboardAsync(string text) { if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.Clipboard is { } clipboard) { try { await clipboard.SetTextAsync(text); return true; } catch { return false; } } return false; } } public sealed record LogVisualizerRow(string Time, string Message, WorkerLogLevel Level);