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 — theAddSignalR()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
WorkerLogmethod toHubBroadcaster
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:
- Created worktree — right after
_wtManager.CreateAsyncsucceeds (around line 69). - Started Claude — just before invoking Claude process.
- Committed changes — after auto-commit (before the
WorktreeUpdatedbroadcast around line 318). - Finished — at both success (line 330) and failure paths, mirroring the existing
TaskFinishedcall.
- 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
WorkerLogEntryrecord
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"
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:
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"
Task 8: Update MainWindow.axaml footer — dock log line right
Files:
-
Modify:
src/ClaudeDo.Ui/Views/MainWindow.axaml(lines 104-135 — the footerBorder) -
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:
- Start a task → expect
HH:MM · Created worktree for "<title>"(dim/info). - Observe while Claude runs → expect
HH:MM · Started Claude for "<title>"(dim/info). - Task commits → expect
HH:MM · Committed changes in "<title>"(dim/info). - Task finishes successfully → expect
HH:MM · Finished "<title>" (done)(green). - Trigger a failing task → expect
HH:MM · Finished "<title>" (failed)(red). - Reset a failed task → expect
HH:MM · Discarded worktree for "<title>"(amber) followed byHH:MM · Reset "<title>"(amber). - Merge a completed task → expect
HH:MM · Merged "<title>" into <branch>(green). - Wait 30s with no new events → footer log line disappears (connection pill remains).
- Trigger a burst of 3 events in quick succession → only the most recent is shown; timer resets on each.
- 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
WorkerLogLevelinClaudeDo.Data— Task 1 ✓ - SignalR enum-as-string — Task 2 ✓
HubBroadcaster.WorkerLog— Task 2 ✓- 7 emit sites with correct level mapping — Tasks 3, 4 ✓
WorkerClient.WorkerLogReceivedevent +WorkerLogEntryrecord — Task 5 ✓WorkerLogLevelToBrushConverterwith 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 ✓
- Enum
-
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.