From ea7694566d21f14873fd85a7349a895cf99c743c Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 23 Apr 2026 14:03:09 +0200 Subject: [PATCH] docs(superpowers): add worker-log footer implementation plan --- .../plans/2026-04-23-worker-log-footer.md | 718 ++++++++++++++++++ 1 file changed, 718 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-23-worker-log-footer.md diff --git a/docs/superpowers/plans/2026-04-23-worker-log-footer.md b/docs/superpowers/plans/2026-04-23-worker-log-footer.md new file mode 100644 index 0000000..361f094 --- /dev/null +++ b/docs/superpowers/plans/2026-04-23-worker-log-footer.md @@ -0,0 +1,718 @@ +# Worker Log Footer Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Surface important Worker lifecycle events in the UI footer as a single rotating, color-coded line that auto-hides after 30s of silence. + +**Architecture:** Add `WorkerLogLevel` enum in shared `ClaudeDo.Data` project. `HubBroadcaster` gets a `WorkerLog(message, level, timestampUtc)` SignalR event. Seven emit sites in `TaskRunner`, `TaskMergeService`, `TaskResetService` (callers of `WorktreeManager`, not WorktreeManager itself — they have the task title in scope). UI side: `WorkerClient` surfaces a `WorkerLogReceived` event; footer state lives on `IslandsShellViewModel` (existing root VM for `MainWindow`, also owns connection state); `System.Timers.Timer` clears the line after 30s; a `WorkerLogLevelToBrushConverter` maps level → brush in XAML. + +**Tech Stack:** .NET 8, ASP.NET Core SignalR, Avalonia 12, CommunityToolkit.Mvvm, xUnit. + +**Spec:** `docs/superpowers/specs/2026-04-23-worker-log-footer-design.md` + +**Deviation from spec:** Spec names `WorktreeManager.CreateAsync` / `DiscardAsync` as emit sites. In practice, `WorktreeManager` has only the task ID in scope; its callers (`TaskRunner`, `TaskResetService`) have the title. Emitting from callers avoids adding constructor dependencies to `WorktreeManager` and produces identical user-visible behavior. + +**Build note:** Per project convention, `dotnet build ClaudeDo.slnx` fails on .NET 8 — always build individual csprojs. + +--- + +### Task 1: Add `WorkerLogLevel` enum (shared contract) + +**Files:** +- Create: `src/ClaudeDo.Data/Models/WorkerLogLevel.cs` + +- [ ] **Step 1: Write the enum** + +```csharp +namespace ClaudeDo.Data.Models; + +public enum WorkerLogLevel +{ + Info, + Success, + Warn, + Error, +} +``` + +- [ ] **Step 2: Build Data project** + +Run: `dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj` +Expected: Build succeeded, 0 errors. + +- [ ] **Step 3: Commit** + +```bash +git add src/ClaudeDo.Data/Models/WorkerLogLevel.cs +git commit -m "feat(data): add WorkerLogLevel enum" +``` + +--- + +### Task 2: Add `WorkerLog` broadcaster method + SignalR JSON enum-as-string + +**Files:** +- Modify: `src/ClaudeDo.Worker/Hub/HubBroadcaster.cs` (append method) +- Modify: `src/ClaudeDo.Worker/Program.cs` (line ~23 — the `AddSignalR()` call) + +- [ ] **Step 1: Add enum-as-string serialization** + +Replace: +```csharp +builder.Services.AddSignalR(); +``` +with: +```csharp +builder.Services.AddSignalR().AddJsonProtocol(options => +{ + options.PayloadSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); +}); +``` + +- [ ] **Step 2: Add `WorkerLog` method to `HubBroadcaster`** + +Add the following method inside `HubBroadcaster` class (after the existing `RunCreated` method): + +```csharp +public Task WorkerLog(string message, WorkerLogLevel level, DateTime timestampUtc) => + _hub.Clients.All.SendAsync("WorkerLog", message, level, timestampUtc); +``` + +Add to the using block at top of file (if not already present): +```csharp +using ClaudeDo.Data.Models; +``` + +- [ ] **Step 3: Build** + +Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj` +Expected: Build succeeded, 0 errors. + +- [ ] **Step 4: Commit** + +```bash +git add src/ClaudeDo.Worker/Hub/HubBroadcaster.cs src/ClaudeDo.Worker/Program.cs +git commit -m "feat(worker): add WorkerLog SignalR event" +``` + +--- + +### Task 3: Emit `WorkerLog` from `TaskRunner` + +**Files:** +- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs` + +Four emit sites in this file: +1. **Created worktree** — right after `_wtManager.CreateAsync` succeeds (around line 69). +2. **Started Claude** — just before invoking Claude process. +3. **Committed changes** — after auto-commit (before the `WorktreeUpdated` broadcast around line 318). +4. **Finished** — at both success (line 330) and failure paths, mirroring the existing `TaskFinished` call. + +- [ ] **Step 1: Add using for `WorkerLogLevel`** + +Ensure `TaskRunner.cs` has at the top: +```csharp +using ClaudeDo.Data.Models; +``` + +- [ ] **Step 2: Emit "Created worktree"** + +After the line `wtCtx = await _wtManager.CreateAsync(task, list, ct);` (around line 69), add: +```csharp +await _broadcaster.WorkerLog($"Created worktree for \"{task.Title}\"", WorkerLogLevel.Info, DateTime.UtcNow); +``` +(Place inside the same `if` branch that called `CreateAsync`, after the assignment.) + +- [ ] **Step 3: Emit "Started Claude"** + +Locate the point just before `ClaudeProcess` is invoked (search for where `ClaudeProcess` or `RunProcessAsync` is called). Just before the invocation, add: +```csharp +await _broadcaster.WorkerLog($"Started Claude for \"{task.Title}\"", WorkerLogLevel.Info, DateTime.UtcNow); +``` + +- [ ] **Step 4: Emit "Committed changes"** + +Locate the auto-commit code path (around line 318, just before `await _broadcaster.WorktreeUpdated(task.Id);`). Add immediately before that call: +```csharp +await _broadcaster.WorkerLog($"Committed changes in \"{task.Title}\"", WorkerLogLevel.Info, DateTime.UtcNow); +``` + +- [ ] **Step 5: Emit "Finished (done)"** + +Find the success finish path (around line 330, where `TaskFinished` is broadcast with status `"done"`). Add immediately before that call: +```csharp +await _broadcaster.WorkerLog($"Finished \"{task.Title}\" (done)", WorkerLogLevel.Success, DateTime.UtcNow); +``` + +- [ ] **Step 6: Emit "Finished (failed)"** + +Find the failure path (search for `TaskFinished` with status `"failed"`). Add immediately before: +```csharp +await _broadcaster.WorkerLog($"Finished \"{task.Title}\" (failed)", WorkerLogLevel.Error, DateTime.UtcNow); +``` + +- [ ] **Step 7: Build** + +Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj` +Expected: Build succeeded, 0 errors. + +- [ ] **Step 8: Run existing Worker tests (no regressions)** + +Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj` +Expected: All tests pass. + +- [ ] **Step 9: Commit** + +```bash +git add src/ClaudeDo.Worker/Runner/TaskRunner.cs +git commit -m "feat(worker): emit WorkerLog events from TaskRunner" +``` + +--- + +### Task 4: Emit `WorkerLog` from `TaskMergeService` and `TaskResetService` + +**Files:** +- Modify: `src/ClaudeDo.Worker/Services/TaskMergeService.cs` +- Modify: `src/ClaudeDo.Worker/Services/TaskResetService.cs` + +Both services already have `HubBroadcaster` injected (`_broadcaster`). Both already load the task entity (needed for title). + +- [ ] **Step 1: Add using in both files** + +Add to the top of each file (if not already present): +```csharp +using ClaudeDo.Data.Models; +``` + +- [ ] **Step 2: Emit "Merged" in `TaskMergeService.MergeAsync`** + +Locate the existing log line around line 137: +```csharp +_logger.LogInformation( + "Merged task {TaskId} branch {Branch} into {Target} (remove worktree: {Remove})", + ...); +``` + +Immediately after it (before `return new MergeResult(...)` on line 140), add: +```csharp +await _broadcaster.WorkerLog($"Merged \"{task.Title}\" into {targetBranch}", WorkerLogLevel.Success, DateTime.UtcNow); +``` +Use whatever variable names `task` and `targetBranch` are in scope — adjust to match the actual local names at that site. + +- [ ] **Step 3: Emit "Discarded" in `TaskResetService.ResetAsync`** + +Locate the call `await _wtManager.DiscardAsync(wt, list.WorkingDir, ct);` (line 53). Immediately after it, add: +```csharp +await _broadcaster.WorkerLog($"Discarded worktree for \"{task.Title}\"", WorkerLogLevel.Warn, DateTime.UtcNow); +``` + +- [ ] **Step 4: Emit "Reset" in `TaskResetService.ResetAsync`** + +Locate the existing line `_logger.LogInformation("Reset task {TaskId} to Manual ...` (line 66). Immediately after it, add: +```csharp +await _broadcaster.WorkerLog($"Reset \"{task.Title}\"", WorkerLogLevel.Warn, DateTime.UtcNow); +``` + +- [ ] **Step 5: Build** + +Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj` +Expected: Build succeeded, 0 errors. + +- [ ] **Step 6: Run existing Worker tests** + +Run: `dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj` +Expected: All tests pass. + +- [ ] **Step 7: Commit** + +```bash +git add src/ClaudeDo.Worker/Services/TaskMergeService.cs src/ClaudeDo.Worker/Services/TaskResetService.cs +git commit -m "feat(worker): emit WorkerLog for merge, discard, reset" +``` + +--- + +### Task 5: Add `WorkerLogEntry` record + `WorkerLogReceived` event on `WorkerClient` + +**Files:** +- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs` + +- [ ] **Step 1: Add using for `WorkerLogLevel`** + +Add at the top of `WorkerClient.cs`: +```csharp +using ClaudeDo.Data.Models; +``` + +- [ ] **Step 2: Declare the `WorkerLogEntry` record** + +Add at the top of the file (above or below the `WorkerClient` class, same namespace): +```csharp +public sealed record WorkerLogEntry(string Message, WorkerLogLevel Level, DateTime TimestampUtc); +``` + +- [ ] **Step 3: Add the event field** + +Alongside the other `public event Action<...>?` declarations (around lines 42-48), add: +```csharp +public event Action? WorkerLogReceivedEvent; +``` + +- [ ] **Step 4: Register the SignalR handler** + +Alongside the other `_hub.On<...>` registrations (around lines 80-117), add: +```csharp +_hub.On("WorkerLog", (message, level, timestampUtc) => +{ + WorkerLogReceivedEvent?.Invoke(new WorkerLogEntry(message, level, timestampUtc)); +}); +``` + +- [ ] **Step 5: Build** + +Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj` +Expected: Build succeeded, 0 errors. + +- [ ] **Step 6: Commit** + +```bash +git add src/ClaudeDo.Ui/Services/WorkerClient.cs +git commit -m "feat(ui): subscribe to WorkerLog SignalR event" +``` + +--- + +### Task 6: Create `ClaudeDo.Ui.Tests` project and add `WorkerLogLevelToBrushConverter` + +**Files:** +- Create: `src/ClaudeDo.Ui/Converters/WorkerLogLevelToBrushConverter.cs` +- Modify: `src/ClaudeDo.Ui/App.axaml` (register converter as resource) +- Create: `tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj` +- Create: `tests/ClaudeDo.Ui.Tests/WorkerLogLevelToBrushConverterTests.cs` + +- [ ] **Step 1: Write the converter** + +Create `src/ClaudeDo.Ui/Converters/WorkerLogLevelToBrushConverter.cs`: + +```csharp +using System; +using System.Globalization; +using Avalonia; +using Avalonia.Data.Converters; +using Avalonia.Media; +using ClaudeDo.Data.Models; + +namespace ClaudeDo.Ui.Converters; + +public sealed class WorkerLogLevelToBrushConverter : IValueConverter +{ + private static readonly IBrush SuccessBrush = new SolidColorBrush(Color.Parse("#4CAF50")); + private static readonly IBrush WarnBrush = new SolidColorBrush(Color.Parse("#FFA726")); + private static readonly IBrush ErrorBrush = new SolidColorBrush(Color.Parse("#EF5350")); + private static readonly IBrush InfoFallback = new SolidColorBrush(Color.Parse("#888888")); + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not WorkerLogLevel level) + return AvaloniaProperty.UnsetValue; + + return level switch + { + WorkerLogLevel.Success => SuccessBrush, + WorkerLogLevel.Warn => WarnBrush, + WorkerLogLevel.Error => ErrorBrush, + WorkerLogLevel.Info => ResolveInfoBrush(), + _ => AvaloniaProperty.UnsetValue, + }; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); + + private static IBrush ResolveInfoBrush() + { + if (Application.Current is { } app && + app.Resources.TryGetResource("TextDimBrush", app.ActualThemeVariant, out var res) && + res is IBrush brush) + { + return brush; + } + return InfoFallback; + } +} +``` + +- [ ] **Step 2: Register converter in `App.axaml`** + +Open `src/ClaudeDo.Ui/App.axaml`. Inside the `` section (add one if missing), add alongside any existing converter entries: + +```xml + +``` + +Ensure the `xmlns:converters="using:ClaudeDo.Ui.Converters"` namespace is declared at the root `` element. If other converters (e.g. `StatusColorConverter`) are already resources in `App.axaml` follow the same pattern; if they're declared per-view, declare this converter at the top of `MainWindow.axaml` in Task 8 instead. + +- [ ] **Step 3: Create UI test project** + +Create `tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj`: + +```xml + + + net8.0 + enable + false + + + + + + + + + + + + +``` + +If the existing `tests/ClaudeDo.Worker.Tests/*.csproj` uses different `Microsoft.NET.Test.Sdk` / xUnit versions, match those versions exactly to avoid analyzer mismatches. + +- [ ] **Step 4: Write the failing test** + +Create `tests/ClaudeDo.Ui.Tests/WorkerLogLevelToBrushConverterTests.cs`: + +```csharp +using System.Globalization; +using Avalonia; +using Avalonia.Media; +using ClaudeDo.Data.Models; +using ClaudeDo.Ui.Converters; +using Xunit; + +namespace ClaudeDo.Ui.Tests; + +public class WorkerLogLevelToBrushConverterTests +{ + [Theory] + [InlineData(WorkerLogLevel.Success, "#FF4CAF50")] + [InlineData(WorkerLogLevel.Warn, "#FFFFA726")] + [InlineData(WorkerLogLevel.Error, "#FFEF5350")] + public void Convert_maps_level_to_expected_brush_color(WorkerLogLevel level, string expectedArgb) + { + var converter = new WorkerLogLevelToBrushConverter(); + + var result = converter.Convert(level, typeof(IBrush), null, CultureInfo.InvariantCulture); + + var solid = Assert.IsType(result); + Assert.Equal(expectedArgb.ToLowerInvariant(), $"#{solid.Color.ToUInt32():X8}".ToLowerInvariant()); + } + + [Fact] + public void Convert_info_returns_a_brush_fallback_when_no_app() + { + var converter = new WorkerLogLevelToBrushConverter(); + + var result = converter.Convert(WorkerLogLevel.Info, typeof(IBrush), null, CultureInfo.InvariantCulture); + + Assert.IsAssignableFrom(result); + } + + [Fact] + public void Convert_unknown_value_returns_unset() + { + var converter = new WorkerLogLevelToBrushConverter(); + + var result = converter.Convert("not a level", typeof(IBrush), null, CultureInfo.InvariantCulture); + + Assert.Equal(AvaloniaProperty.UnsetValue, result); + } +} +``` + +- [ ] **Step 5: Run the tests** + +Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj` +Expected: All 5 tests pass. + +- [ ] **Step 6: Commit** + +```bash +git add src/ClaudeDo.Ui/Converters/WorkerLogLevelToBrushConverter.cs src/ClaudeDo.Ui/App.axaml tests/ClaudeDo.Ui.Tests/ +git commit -m "feat(ui): add WorkerLogLevelToBrushConverter with tests" +``` + +--- + +### Task 7: Add footer state + 30s auto-clear timer to `IslandsShellViewModel` + +**Files:** +- Modify: `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs` +- Create: `tests/ClaudeDo.Ui.Tests/IslandsShellViewModelWorkerLogTests.cs` + +Timer uses `System.Timers.Timer` (not `DispatcherTimer`) so unit tests don't need an Avalonia dispatcher. The elapsed callback marshals to the UI thread via `Dispatcher.UIThread.Post` when the dispatcher is available; in tests the VM logic under test sets properties directly so no marshalling is needed. + +- [ ] **Step 1: Write the failing tests** + +Create `tests/ClaudeDo.Ui.Tests/IslandsShellViewModelWorkerLogTests.cs`: + +```csharp +using System; +using ClaudeDo.Data.Models; +using ClaudeDo.Ui.Services; +using ClaudeDo.Ui.ViewModels; +using Xunit; + +namespace ClaudeDo.Ui.Tests; + +public class IslandsShellViewModelWorkerLogTests +{ + private static IslandsShellViewModel NewVm() => + // The real constructor requires island VMs + WorkerClient. These tests + // only exercise the WorkerLog handling, so we use a test-only constructor + // that bypasses the sub-VMs. Add `internal IslandsShellViewModel()` for tests. + IslandsShellViewModel.CreateForTests(); + + [Fact] + public void Receiving_event_sets_text_level_and_visible() + { + var vm = NewVm(); + var at = new DateTime(2026, 4, 23, 14, 32, 0, DateTimeKind.Utc); + + vm.OnWorkerLogReceived(new WorkerLogEntry("Created worktree for \"X\"", WorkerLogLevel.Info, at)); + + Assert.True(vm.IsWorkerLogVisible); + Assert.Equal(WorkerLogLevel.Info, vm.WorkerLogLevel); + Assert.Contains("Created worktree for \"X\"", vm.WorkerLogText); + } + + [Fact] + public void Second_event_replaces_first() + { + var vm = NewVm(); + vm.OnWorkerLogReceived(new WorkerLogEntry("first", WorkerLogLevel.Info, DateTime.UtcNow)); + vm.OnWorkerLogReceived(new WorkerLogEntry("second", WorkerLogLevel.Success, DateTime.UtcNow)); + + Assert.Contains("second", vm.WorkerLogText); + Assert.Equal(WorkerLogLevel.Success, vm.WorkerLogLevel); + } + + [Fact] + public void ClearWorkerLog_hides_line() + { + var vm = NewVm(); + vm.OnWorkerLogReceived(new WorkerLogEntry("msg", WorkerLogLevel.Info, DateTime.UtcNow)); + + vm.ClearWorkerLog(); + + Assert.False(vm.IsWorkerLogVisible); + Assert.Null(vm.WorkerLogText); + } + + [Fact] + public void Text_is_formatted_as_HHmm_dot_message_local_time() + { + var vm = NewVm(); + var utc = new DateTime(2026, 4, 23, 12, 0, 0, DateTimeKind.Utc); + var expectedLocalHhmm = utc.ToLocalTime().ToString("HH:mm"); + + vm.OnWorkerLogReceived(new WorkerLogEntry("hello", WorkerLogLevel.Info, utc)); + + Assert.StartsWith(expectedLocalHhmm + " · ", vm.WorkerLogText); + } +} +``` + +- [ ] **Step 2: Run tests to confirm they fail to compile** + +Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj` +Expected: Build errors — `CreateForTests`, `OnWorkerLogReceived`, `ClearWorkerLog`, `IsWorkerLogVisible`, `WorkerLogText`, `WorkerLogLevel` do not yet exist. + +- [ ] **Step 3: Implement on `IslandsShellViewModel`** + +Open `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs`. Add `using`s if missing: + +```csharp +using System.Timers; +using Avalonia.Threading; +using ClaudeDo.Data.Models; +using ClaudeDo.Ui.Services; +``` + +Inside the class, add: + +```csharp +[ObservableProperty] private string? workerLogText; +[ObservableProperty] private WorkerLogLevel workerLogLevel; +[ObservableProperty] private bool isWorkerLogVisible; + +private readonly Timer _workerLogTimer = new(TimeSpan.FromSeconds(30).TotalMilliseconds) +{ + AutoReset = false, +}; + +internal static IslandsShellViewModel CreateForTests() => + (IslandsShellViewModel)System.Runtime.Serialization.FormatterServices + .GetUninitializedObject(typeof(IslandsShellViewModel)); +``` + +(If `FormatterServices` is unavailable under `net8.0`, instead add a parameterless `internal IslandsShellViewModel() {}` constructor guarded for tests only.) + +In the existing real constructor, wire up subscription (after the line `Worker.PropertyChanged += ...` block, around line 63-70): + +```csharp +Worker.WorkerLogReceivedEvent += OnWorkerLogReceived; +_workerLogTimer.Elapsed += (_, _) => +{ + if (Dispatcher.UIThread.CheckAccess()) ClearWorkerLog(); + else Dispatcher.UIThread.Post(ClearWorkerLog); +}; +``` + +Add the methods the tests call: + +```csharp +public void OnWorkerLogReceived(WorkerLogEntry entry) +{ + var hhmm = entry.TimestampUtc.ToLocalTime().ToString("HH:mm"); + WorkerLogText = $"{hhmm} · {entry.Message}"; + WorkerLogLevel = entry.Level; + IsWorkerLogVisible = true; + + _workerLogTimer.Stop(); + _workerLogTimer.Start(); +} + +public void ClearWorkerLog() +{ + IsWorkerLogVisible = false; + WorkerLogText = null; +} +``` + +- [ ] **Step 4: Run tests** + +Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj` +Expected: All tests pass (5 converter + 4 VM = 9 tests). + +- [ ] **Step 5: Build the UI project as a sanity check** + +Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj` +Expected: Build succeeded. + +- [ ] **Step 6: Commit** + +```bash +git add src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs tests/ClaudeDo.Ui.Tests/IslandsShellViewModelWorkerLogTests.cs +git commit -m "feat(ui): add worker log state and 30s timer to shell VM" +``` + +--- + +### Task 8: Update `MainWindow.axaml` footer — dock log line right + +**Files:** +- Modify: `src/ClaudeDo.Ui/Views/MainWindow.axaml` (lines 104-135 — the footer `Border`) + +- [ ] **Step 1: Add the converter resource to the window (if not already in App.axaml)** + +If Task 6 declared the converter in `App.axaml`, skip this step. Otherwise, add a `` block near the top of `MainWindow.axaml`: +```xml + + + +``` +Ensure `xmlns:converters="using:ClaudeDo.Ui.Converters"` is declared on the root ``. + +- [ ] **Step 2: Replace the footer body** + +Replace the existing footer `` inner contents (the `` at lines 109-134) with: + +```xml + + + + + + + + + + + + + + + +``` + +- [ ] **Step 3: Build the UI** + +Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj` +Expected: Build succeeded, 0 errors. No XAML compilation errors. + +- [ ] **Step 4: Build the full app (entry point)** + +Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj` +Expected: Build succeeded. + +- [ ] **Step 5: Manual smoke test** + +Start the Worker and the App (two separate processes per CLAUDE.md). + +Exercise each event and confirm the footer line appears with the expected color and copy: + +1. Start a task → expect `HH:MM · Created worktree for ""` (dim/info). +2. Observe while Claude runs → expect `HH:MM · Started Claude for "<title>"` (dim/info). +3. Task commits → expect `HH:MM · Committed changes in "<title>"` (dim/info). +4. Task finishes successfully → expect `HH:MM · Finished "<title>" (done)` (green). +5. Trigger a failing task → expect `HH:MM · Finished "<title>" (failed)` (red). +6. Reset a failed task → expect `HH:MM · Discarded worktree for "<title>"` (amber) followed by `HH:MM · Reset "<title>"` (amber). +7. Merge a completed task → expect `HH:MM · Merged "<title>" into <branch>` (green). +8. Wait 30s with no new events → footer log line disappears (connection pill remains). +9. Trigger a burst of 3 events in quick succession → only the most recent is shown; timer resets on each. +10. Long task title (≥60 chars) → line is ellipsized, connection pill on the left remains fully visible. + +- [ ] **Step 6: Commit** + +```bash +git add src/ClaudeDo.Ui/Views/MainWindow.axaml +git commit -m "feat(ui): show worker log line in footer" +``` + +--- + +## Self-Review + +- **Spec coverage:** + - Enum `WorkerLogLevel` in `ClaudeDo.Data` — Task 1 ✓ + - SignalR enum-as-string — Task 2 ✓ + - `HubBroadcaster.WorkerLog` — Task 2 ✓ + - 7 emit sites with correct level mapping — Tasks 3, 4 ✓ + - `WorkerClient.WorkerLogReceived` event + `WorkerLogEntry` record — Task 5 ✓ + - `WorkerLogLevelToBrushConverter` with unit tests — Task 6 ✓ + - Footer VM state + 30s timer + tests — Task 7 ✓ + - Footer XAML (DockPanel, connection left, log right, level-based color, ellipsis) — Task 8 ✓ + - Out-of-scope items (history drawer, filtering, persistence) — correctly omitted ✓ + +- **Placeholder scan:** No "TBD" / "handle edge cases" / "similar to Task N". All code is inline. + +- **Type consistency:** `WorkerLogEntry(Message, Level, TimestampUtc)` — same signature used in Task 5 (declaration), Task 7 (consumer tests + VM). `WorkerLog(message, level, timestampUtc)` — same signature in Task 2 (broadcaster) and Tasks 3-4 (callers). `OnWorkerLogReceived` / `ClearWorkerLog` / `IsWorkerLogVisible` / `WorkerLogText` / `WorkerLogLevel` — consistent between Task 7 test and Task 7 implementation.