diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json
index 2407f09..db873dc 100644
--- a/src/ClaudeDo.Localization/locales/de.json
+++ b/src/ClaudeDo.Localization/locales/de.json
@@ -234,6 +234,15 @@
"reviewResetTip": "Alle Änderungen verwerfen und die Aufgabe auf Leerlauf zurücksetzen"
},
"modals": {
+ "logVisualizer": {
+ "title": "WORKER-LOGS — LETZTE 30 MIN",
+ "warnErrorOnly": "Nur Warnungen & Fehler",
+ "refresh": "Aktualisieren",
+ "empty": "Keine Logs in den letzten 30 Minuten.",
+ "count": "{0} Einträge",
+ "footerHint": "logs",
+ "openTooltip": "Aktuelle Worker-Logs anzeigen"
+ },
"about": {
"title": "ÜBER",
"version": "Version",
diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json
index 89bfeea..f1043aa 100644
--- a/src/ClaudeDo.Localization/locales/en.json
+++ b/src/ClaudeDo.Localization/locales/en.json
@@ -234,6 +234,15 @@
"reviewResetTip": "Discard all changes and reset the task to Idle"
},
"modals": {
+ "logVisualizer": {
+ "title": "WORKER LOGS — LAST 30 MIN",
+ "warnErrorOnly": "Warnings & errors only",
+ "refresh": "Refresh",
+ "empty": "No logs in the last 30 minutes.",
+ "count": "{0} entries",
+ "footerHint": "logs",
+ "openTooltip": "View recent worker logs"
+ },
"about": {
"title": "ABOUT",
"version": "Version",
diff --git a/src/ClaudeDo.Ui/Services/IDialogService.cs b/src/ClaudeDo.Ui/Services/IDialogService.cs
index 94fe0a2..090fc09 100644
--- a/src/ClaudeDo.Ui/Services/IDialogService.cs
+++ b/src/ClaudeDo.Ui/Services/IDialogService.cs
@@ -21,6 +21,7 @@ public interface IDialogService
Task ShowWorktreesOverviewAsync(WorktreesOverviewModalViewModel vm);
Task ShowWorkerConnectionAsync(WorkerConnectionModalViewModel vm);
Task ShowConflictResolverAsync(ConflictResolverViewModel vm);
+ Task ShowLogVisualizerAsync(LogVisualizerViewModel vm);
/// Modal yes/no confirmation. Returns true only when confirmed.
Task ConfirmAsync(string message);
diff --git a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs
index d357001..ee10573 100644
--- a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs
+++ b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs
@@ -85,6 +85,7 @@ public interface IWorkerClient : INotifyPropertyChanged
Task UpdateDailyNoteAsync(string id, string text);
Task DeleteDailyNoteAsync(string id);
Task GetLastPrepLogAsync();
+ Task> GetRecentLogsAsync();
Task> GetPrimeSchedulesAsync();
Task UpsertPrimeScheduleAsync(PrimeScheduleDto dto);
diff --git a/src/ClaudeDo.Ui/Services/WorkerClient.cs b/src/ClaudeDo.Ui/Services/WorkerClient.cs
index 9d12f5c..9f88fcc 100644
--- a/src/ClaudeDo.Ui/Services/WorkerClient.cs
+++ b/src/ClaudeDo.Ui/Services/WorkerClient.cs
@@ -388,6 +388,9 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
public async Task GetLastPrepLogAsync()
=> await TryInvokeAsync("GetLastPrepLog") ?? string.Empty;
+ public async Task> GetRecentLogsAsync()
+ => await TryInvokeAsync>("GetRecentLogs") ?? new List();
+
public async Task UpdateListAsync(UpdateListDto dto)
{
await _hub.InvokeAsync("UpdateList", dto);
diff --git a/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs b/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs
index c97d891..c5b1d38 100644
--- a/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs
@@ -290,6 +290,15 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
if (Dialogs is not null) await Dialogs.ShowAboutAsync(vm);
}
+ [RelayCommand]
+ private async Task OpenLogVisualizer()
+ {
+ if (Dialogs is null || Worker is null) return;
+ var vm = new LogVisualizerViewModel(Worker);
+ await vm.RefreshAsync();
+ await Dialogs.ShowLogVisualizerAsync(vm);
+ }
+
private bool _connectionPromptShown;
internal bool DecideShowConnectionPrompt(bool isOffline)
diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/LogVisualizerViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/LogVisualizerViewModel.cs
new file mode 100644
index 0000000..a979dd5
--- /dev/null
+++ b/src/ClaudeDo.Ui/ViewModels/Modals/LogVisualizerViewModel.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading.Tasks;
+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 readonly IWorkerClient _worker;
+ 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) => _worker = worker;
+
+ [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)
+ 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() => CloseAction?.Invoke();
+}
+
+public sealed record LogVisualizerRow(string Time, string Message, WorkerLogLevel Level);
diff --git a/src/ClaudeDo.Ui/Views/MainWindow.axaml b/src/ClaudeDo.Ui/Views/MainWindow.axaml
index 5318f25..21c3728 100644
--- a/src/ClaudeDo.Ui/Views/MainWindow.axaml
+++ b/src/ClaudeDo.Ui/Views/MainWindow.axaml
@@ -215,15 +215,28 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ClaudeDo.Ui/Views/Modals/LogVisualizerView.axaml.cs b/src/ClaudeDo.Ui/Views/Modals/LogVisualizerView.axaml.cs
new file mode 100644
index 0000000..08740f0
--- /dev/null
+++ b/src/ClaudeDo.Ui/Views/Modals/LogVisualizerView.axaml.cs
@@ -0,0 +1,10 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace ClaudeDo.Ui.Views.Modals;
+
+public partial class LogVisualizerView : Window
+{
+ public LogVisualizerView() => InitializeComponent();
+ private void InitializeComponent() => AvaloniaXamlLoader.Load(this);
+}
diff --git a/src/ClaudeDo.Ui/Views/WindowDialogService.cs b/src/ClaudeDo.Ui/Views/WindowDialogService.cs
index cf8a5a9..582ca06 100644
--- a/src/ClaudeDo.Ui/Views/WindowDialogService.cs
+++ b/src/ClaudeDo.Ui/Views/WindowDialogService.cs
@@ -104,6 +104,13 @@ public sealed class WindowDialogService : IDialogService
await dlg.ShowDialog(_owner);
}
+ public async Task ShowLogVisualizerAsync(LogVisualizerViewModel vm)
+ {
+ var dlg = new LogVisualizerView { DataContext = vm };
+ vm.CloseAction = () => dlg.Close();
+ await dlg.ShowDialog(_owner);
+ }
+
public Task ConfirmAsync(string message)
{
var tcs = new TaskCompletionSource();
diff --git a/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs b/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs
index cc166de..049cf95 100644
--- a/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs
+++ b/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs
@@ -77,6 +77,7 @@ public abstract class StubWorkerClient : IWorkerClient
public virtual Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default) => Task.CompletedTask;
public virtual Task GetPendingDraftCountAsync(string taskId, CancellationToken ct = default) => Task.FromResult(0);
public virtual Task GetMergeTargetsAsync(string taskId) => Task.FromResult(null);
+ public virtual Task> GetRecentLogsAsync() => Task.FromResult>(Array.Empty());
public virtual Task> GetPlanningAggregateAsync(string planningTaskId)
=> Task.FromResult>(Array.Empty());
public virtual Task BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch)
diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/LogVisualizerViewModelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/LogVisualizerViewModelTests.cs
new file mode 100644
index 0000000..abd7572
--- /dev/null
+++ b/tests/ClaudeDo.Ui.Tests/ViewModels/LogVisualizerViewModelTests.cs
@@ -0,0 +1,51 @@
+using ClaudeDo.Data.Models;
+using ClaudeDo.Ui.Services;
+using ClaudeDo.Ui.ViewModels.Modals;
+
+namespace ClaudeDo.Ui.Tests.ViewModels;
+
+public class LogVisualizerViewModelTests
+{
+ private sealed class FakeClient : StubWorkerClient
+ {
+ private readonly IReadOnlyList _logs;
+ public FakeClient(IReadOnlyList logs) => _logs = logs;
+ public override Task> GetRecentLogsAsync() => Task.FromResult(_logs);
+ }
+
+ private static WorkerLogEntry E(WorkerLogLevel lvl, string msg)
+ => new(msg, lvl, new DateTime(2026, 6, 23, 8, 0, 0, DateTimeKind.Utc));
+
+ [Fact]
+ public async Task Refresh_populates_rows_from_worker()
+ {
+ var vm = new LogVisualizerViewModel(new FakeClient(new[] { E(WorkerLogLevel.Info, "a"), E(WorkerLogLevel.Error, "b") }));
+
+ await vm.RefreshAsync();
+
+ Assert.Equal(new[] { "a", "b" }, vm.Rows.Select(r => r.Message));
+ }
+
+ [Fact]
+ public async Task WarnErrorOnly_filters_out_info()
+ {
+ var vm = new LogVisualizerViewModel(new FakeClient(new[]
+ { E(WorkerLogLevel.Info, "a"), E(WorkerLogLevel.Warn, "w"), E(WorkerLogLevel.Error, "e") }));
+ await vm.RefreshAsync();
+
+ vm.WarnErrorOnly = true;
+
+ Assert.Equal(new[] { "w", "e" }, vm.Rows.Select(r => r.Message));
+ }
+
+ [Fact]
+ public async Task Empty_logs_yield_no_rows_and_a_status()
+ {
+ var vm = new LogVisualizerViewModel(new FakeClient(Array.Empty()));
+
+ await vm.RefreshAsync();
+
+ Assert.Empty(vm.Rows);
+ Assert.False(string.IsNullOrEmpty(vm.StatusText));
+ }
+}
diff --git a/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs b/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs
index 0128ff4..8bfff79 100644
--- a/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs
@@ -71,6 +71,7 @@ sealed class FakeWorkerClient : IWorkerClient
}
public Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default) { FinalizePlanningCalls++; return Task.CompletedTask; }
public Task GetPendingDraftCountAsync(string taskId, CancellationToken ct = default) => Task.FromResult(0);
+ public Task> GetRecentLogsAsync() => Task.FromResult>(System.Array.Empty());
public event Action? PrepStartedEvent;
public event Action? PrepLineEvent;