From 8c251c78b1463c926dec73296837d99f5c0d1920 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Wed, 5 Aug 2026 20:26:04 +0200 Subject: [PATCH] feat(ui): add copy-last-40-lines button to log visualizer --- src/ClaudeDo.Localization/locales/de.json | 4 + src/ClaudeDo.Localization/locales/en.json | 6 +- .../Modals/LogVisualizerViewModel.cs | 42 ++++++++- .../Views/Modals/LogVisualizerView.axaml | 8 +- .../ViewModels/LogVisualizerViewModelTests.cs | 91 +++++++++++++++++++ 5 files changed, 147 insertions(+), 4 deletions(-) diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index 2504b9f3..ea9c9275 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -310,6 +310,10 @@ "refresh": "Aktualisieren", "empty": "Keine Logs in den letzten 30 Minuten.", "count": "{0} Einträge", + "copyLast": "Letzte 40 kopieren", + "copyLastTooltip": "Kopiert die letzten 40 sichtbaren Zeilen in die Zwischenablage", + "copied": "{0} Zeilen kopiert", + "copyFailed": "Kopieren fehlgeschlagen", "footerHint": "logs", "openTooltip": "Aktuelle Worker-Logs anzeigen" }, diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index 3c9a44e1..7a3e5ee3 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -311,7 +311,11 @@ "empty": "No logs in the last 30 minutes.", "count": "{0} entries", "footerHint": "logs", - "openTooltip": "View recent worker logs" + "openTooltip": "View recent worker logs", + "copyLast": "Copy last 40", + "copyLastTooltip": "Copy the last 40 visible lines to the clipboard", + "copied": "{0} lines copied", + "copyFailed": "Copy failed" }, "about": { "title": "ABOUT", diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/LogVisualizerViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/LogVisualizerViewModel.cs index 88d86bd8..c78fd945 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/LogVisualizerViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/LogVisualizerViewModel.cs @@ -3,6 +3,9 @@ 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; @@ -18,7 +21,10 @@ namespace ClaudeDo.Ui.ViewModels.Modals; /// 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(); @@ -28,7 +34,14 @@ public sealed partial class LogVisualizerViewModel : ViewModelBase public Action? CloseAction { get; set; } - public LogVisualizerViewModel(IWorkerClient worker) => _worker = worker; + public LogVisualizerViewModel(IWorkerClient worker) : this(worker, CopyToClipboardAsync) { } + + internal LogVisualizerViewModel(IWorkerClient worker, Func> copyToClipboard) + { + _worker = worker; + _copyToClipboard = copyToClipboard; + Rows.CollectionChanged += (_, _) => CopyLastCommand.NotifyCanExecuteChanged(); + } [RelayCommand] public async Task RefreshAsync() @@ -53,6 +66,33 @@ public sealed partial class LogVisualizerViewModel : ViewModelBase } [RelayCommand] private void Close() => 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); diff --git a/src/ClaudeDo.Ui/Views/Modals/LogVisualizerView.axaml b/src/ClaudeDo.Ui/Views/Modals/LogVisualizerView.axaml index 7994f223..069ab75f 100644 --- a/src/ClaudeDo.Ui/Views/Modals/LogVisualizerView.axaml +++ b/src/ClaudeDo.Ui/Views/Modals/LogVisualizerView.axaml @@ -22,7 +22,7 @@ - + -