Files
ClaudeDo/docs/superpowers/plans/2026-04-23-worker-log-footer.md

25 KiB

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

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
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:

builder.Services.AddSignalR();

with:

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):

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):

using ClaudeDo.Data.Models;
  • Step 3: Build

Run: dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj Expected: Build succeeded, 0 errors.

  • Step 4: Commit
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:

using ClaudeDo.Data.Models;
  • Step 2: Emit "Created worktree"

After the line wtCtx = await _wtManager.CreateAsync(task, list, ct); (around line 69), add:

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:

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:

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:

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:

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
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):

using ClaudeDo.Data.Models;
  • Step 2: Emit "Merged" in TaskMergeService.MergeAsync

Locate the existing log line around line 137:

_logger.LogInformation(
    "Merged task {TaskId} branch {Branch} into {Target} (remove worktree: {Remove})",
    ...);

Immediately after it (before return new MergeResult(...) on line 140), add:

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:

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:

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
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:

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):

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:

public event Action<WorkerLogEntry>? WorkerLogReceivedEvent;
  • Step 4: Register the SignalR handler

Alongside the other _hub.On<...> registrations (around lines 80-117), add:

_hub.On<string, WorkerLogLevel, DateTime>("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
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:

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 <Application.Resources> section (add one if missing), add alongside any existing converter entries:

<converters:WorkerLogLevelToBrushConverter x:Key="WorkerLogLevelToBrush"/>

Ensure the xmlns:converters="using:ClaudeDo.Ui.Converters" namespace is declared at the root <Application> 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:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Avalonia" Version="12.0.0" />
    <PackageReference Include="Avalonia.Headless" Version="12.0.0" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
    <PackageReference Include="xunit" Version="2.9.2" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\..\src\ClaudeDo.Ui\ClaudeDo.Ui.csproj" />
  </ItemGroup>
</Project>

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:

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<SolidColorBrush>(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<IBrush>(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
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"

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:

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 usings if missing:

using System.Timers;
using Avalonia.Threading;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Services;

Inside the class, add:

[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):

Worker.WorkerLogReceivedEvent += OnWorkerLogReceived;
_workerLogTimer.Elapsed += (_, _) =>
{
    if (Dispatcher.UIThread.CheckAccess()) ClearWorkerLog();
    else Dispatcher.UIThread.Post(ClearWorkerLog);
};

Add the methods the tests call:

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
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"

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 <Window.Resources> block near the top of MainWindow.axaml:

<Window.Resources>
    <converters:WorkerLogLevelToBrushConverter x:Key="WorkerLogLevelToBrush"/>
</Window.Resources>

Ensure xmlns:converters="using:ClaudeDo.Ui.Converters" is declared on the root <Window>.

  • Step 2: Replace the footer body

Replace the existing footer <Border Grid.Row="2" ...> inner contents (the <StackPanel> at lines 109-134) with:

<DockPanel LastChildFill="True" Margin="14,0">
    <!-- Left: connection pill -->
    <StackPanel DockPanel.Dock="Left" Orientation="Horizontal" Spacing="7"
                VerticalAlignment="Center">
        <Ellipse Width="7" Height="7" Fill="#4CAF50"
                 IsVisible="{Binding Worker.IsConnected}"/>
        <Ellipse Width="7" Height="7" Fill="#FFA726"
                 IsVisible="{Binding Worker.IsReconnecting}"/>
        <Ellipse Width="7" Height="7" Fill="#EF5350"
                 IsVisible="{Binding IsOffline}"/>
        <TextBlock Text="{Binding ConnectionText, Converter={StaticResource UpperCase}}"
                   FontFamily="{DynamicResource MonoFont}"
                   FontSize="10"
                   LetterSpacing="1.4"
                   Foreground="{DynamicResource TextDimBrush}"
                   VerticalAlignment="Center"/>
    </StackPanel>

    <!-- Right: worker log line -->
    <TextBlock DockPanel.Dock="Right"
               Text="{Binding WorkerLogText}"
               IsVisible="{Binding IsWorkerLogVisible}"
               Foreground="{Binding WorkerLogLevel, Converter={StaticResource WorkerLogLevelToBrush}}"
               FontFamily="{DynamicResource MonoFont}"
               FontSize="10"
               LetterSpacing="1.4"
               TextTrimming="CharacterEllipsis"
               VerticalAlignment="Center"/>

    <!-- Spacer (fills remaining space between pill and log) -->
    <Panel/>
</DockPanel>
  • 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 "<title>" (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
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.