fix(claude-do): merge Log Visualizer shows every footer-log message, with footer c
ClaudeDo-Task: 199f5a36ad8a4f329fc8bdeac10c5304
This commit is contained in:
@@ -63,7 +63,7 @@ Design/ — Tokens.axaml (design tokens; merged before styles)
|
||||
|
||||
Self-explanatory: `RepoImportModalViewModel` (bulk-create lists from git repos; already-wired
|
||||
repos disabled), `MergeModalViewModel`, `WorktreesOverviewModalViewModel`,
|
||||
`UnfinishedPlanningModalViewModel`, `LogVisualizerViewModel` (last 30 min, all levels + a
|
||||
`UnfinishedPlanningModalViewModel`, `LogVisualizerViewModel` (last 30 min, all levels, merged with the shell's footer-strip history — worker Warn/Error copies deduped by record equality — + a
|
||||
warn/error filter), `WorkerConnectionModalViewModel`, `AboutModalViewModel`.
|
||||
|
||||
`CheatSheetsViewModel` backs the modeless `CheatSheetsWindow` (Help → Cheat Sheets): content is
|
||||
|
||||
@@ -181,8 +181,23 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
||||
OnPropertyChanged(nameof(TasksColumnSpan));
|
||||
}
|
||||
|
||||
private const int FooterHistoryCap = 500;
|
||||
|
||||
/// <summary>Every line the footer strip has shown this session, oldest-first, capped; UI thread only.</summary>
|
||||
private readonly List<WorkerLogEntry> _footerHistory = new();
|
||||
|
||||
internal IReadOnlyList<WorkerLogEntry> FooterHistorySnapshot() => _footerHistory.ToArray();
|
||||
|
||||
private void RecordFooter(WorkerLogEntry entry)
|
||||
{
|
||||
_footerHistory.Add(entry);
|
||||
if (_footerHistory.Count > FooterHistoryCap)
|
||||
_footerHistory.RemoveAt(0);
|
||||
}
|
||||
|
||||
public void OnWorkerLogReceived(WorkerLogEntry entry)
|
||||
{
|
||||
RecordFooter(entry);
|
||||
var hhmm = entry.TimestampUtc.ToLocalTime().ToString("HH:mm");
|
||||
WorkerLogText = $"{hhmm} · {entry.Message}";
|
||||
WorkerLogLevel = entry.Level;
|
||||
@@ -214,6 +229,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
||||
if (level == WorkerLogLevel.Error)
|
||||
Serilog.Log.Warning("Footer error: {message}", message);
|
||||
|
||||
RecordFooter(new WorkerLogEntry(message, level, DateTime.UtcNow));
|
||||
WorkerLogText = $"{DateTime.Now:HH:mm} · {message}";
|
||||
WorkerLogLevel = level;
|
||||
IsWorkerLogVisible = true;
|
||||
@@ -541,7 +557,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
||||
private async Task OpenLogVisualizer()
|
||||
{
|
||||
if (Dialogs is null || Worker is null) return;
|
||||
var vm = new LogVisualizerViewModel(Worker);
|
||||
var vm = new LogVisualizerViewModel(Worker, FooterHistorySnapshot);
|
||||
await vm.RefreshAsync();
|
||||
await Dialogs.ShowLogVisualizerAsync(vm);
|
||||
}
|
||||
|
||||
@@ -15,15 +15,16 @@ using CommunityToolkit.Mvvm.Input;
|
||||
namespace ClaudeDo.Ui.ViewModels.Modals;
|
||||
|
||||
/// <summary>
|
||||
/// Log Visualizer overlay — shows the worker's last 30 min of log records (all levels),
|
||||
/// fetched once on open via <see cref="IWorkerClient.GetRecentLogsAsync"/> with a manual
|
||||
/// Refresh and a "warnings & errors only" filter.
|
||||
/// Log Visualizer overlay — shows the worker's last 30 min of log records (all levels, via
|
||||
/// <see cref="IWorkerClient.GetRecentLogsAsync"/>) merged with the shell's footer-strip history,
|
||||
/// with a manual Refresh, a 4 s tick and a "warnings & errors only" filter.
|
||||
/// </summary>
|
||||
public sealed partial class LogVisualizerViewModel : ViewModelBase
|
||||
{
|
||||
private const int CopyLastCount = 40;
|
||||
|
||||
private readonly IWorkerClient _worker;
|
||||
private readonly Func<IReadOnlyList<WorkerLogEntry>> _footerHistory;
|
||||
private readonly Func<string, Task<bool>> _copyToClipboard;
|
||||
private IReadOnlyList<WorkerLogEntry> _all = Array.Empty<WorkerLogEntry>();
|
||||
|
||||
@@ -34,11 +35,15 @@ public sealed partial class LogVisualizerViewModel : ViewModelBase
|
||||
|
||||
public Action? CloseAction { get; set; }
|
||||
|
||||
public LogVisualizerViewModel(IWorkerClient worker) : this(worker, CopyToClipboardAsync) { }
|
||||
/// <summary>Creates the overlay; <paramref name="footerHistory"/> returns the shell's footer lines oldest-first.</summary>
|
||||
public LogVisualizerViewModel(IWorkerClient worker, Func<IReadOnlyList<WorkerLogEntry>> footerHistory)
|
||||
: this(worker, footerHistory, CopyToClipboardAsync) { }
|
||||
|
||||
internal LogVisualizerViewModel(IWorkerClient worker, Func<string, Task<bool>> copyToClipboard)
|
||||
internal LogVisualizerViewModel(IWorkerClient worker, Func<IReadOnlyList<WorkerLogEntry>> footerHistory,
|
||||
Func<string, Task<bool>> copyToClipboard)
|
||||
{
|
||||
_worker = worker;
|
||||
_footerHistory = footerHistory;
|
||||
_copyToClipboard = copyToClipboard;
|
||||
Rows.CollectionChanged += (_, _) => CopyLastCommand.NotifyCanExecuteChanged();
|
||||
// Phase 3 reconcile tick: this overlay is long-lived (stays open while the user reads
|
||||
@@ -55,7 +60,18 @@ public sealed partial class LogVisualizerViewModel : ViewModelBase
|
||||
public async Task RefreshAsync()
|
||||
{
|
||||
var latest = await _worker.GetRecentLogsAsync();
|
||||
Reconcile(latest);
|
||||
Reconcile(Merge(latest));
|
||||
}
|
||||
|
||||
// Worker Warn/Error lines reach the footer through BroadcastLogSink with the exact message,
|
||||
// level and timestamp of their ring-buffer copy, so record equality drops the duplicate.
|
||||
// OrderBy is stable: buffer entries precede footer entries on a timestamp tie, which keeps
|
||||
// the merged order identical across ticks (Reconcile's overlap match depends on that).
|
||||
private List<WorkerLogEntry> Merge(IReadOnlyList<WorkerLogEntry> buffer)
|
||||
{
|
||||
var known = buffer.ToHashSet();
|
||||
return buffer.Concat(_footerHistory().Where(e => !known.Contains(e)))
|
||||
.OrderBy(e => e.TimestampUtc).ToList();
|
||||
}
|
||||
|
||||
partial void OnWarnErrorOnlyChanged(bool value) => Apply();
|
||||
@@ -71,11 +87,13 @@ public sealed partial class LogVisualizerViewModel : ViewModelBase
|
||||
UpdateStatusText();
|
||||
}
|
||||
|
||||
// `_all` (oldest-first) mirrors the worker's LogRingBuffer: append-mostly, with the oldest
|
||||
// `_all` (oldest-first) is the merged buffer + footer list: append-mostly, with the oldest
|
||||
// entries falling off the front once they age past the 30-min window. A full Rows.Clear()
|
||||
// on every 4s tick would reset the user's scroll position/selection while they're mid-read,
|
||||
// so patch just the delta instead: entries still present keep their row, aged-out entries
|
||||
// are trimmed off the (oldest) tail of Rows, new entries are inserted at the (newest) head.
|
||||
// An entry merged into the middle shrinks the overlap, so more rows are dropped and
|
||||
// re-added — still correct, it just doesn't preserve scroll for that rare tick.
|
||||
private void Reconcile(IReadOnlyList<WorkerLogEntry> latest)
|
||||
{
|
||||
var previous = _all;
|
||||
|
||||
Reference in New Issue
Block a user