6.6 KiB
Worker Log Footer — Design
Date: 2026-04-23
Goal
Surface important Worker lifecycle events (worktree created, Claude started, merged, etc.) in the UI footer as a single rotating, color-coded line. Gives the user ambient awareness of what the Worker just did without opening task details.
Non-Goals
- No log history, drawer, or scrollback
- No filtering or user-configurable verbosity
- No persistence across UI restarts
- No replay of events missed while UI was disconnected
UX
Footer (MainWindow.axaml, row 2) layout changes from StackPanel to DockPanel:
- Docked left: existing connection pill (ellipse +
ONLINE/OFFLINE/RECONNECTINGtext). The static· WORKERlabel is removed; the rotating log line replaces its purpose. - Docked right: rotating worker-log line.
Line format: 14:32 · <message>, rendered in the mono font at size 10 (matches existing footer typography). TextTrimming="CharacterEllipsis" so long task titles don't push out the connection pill.
The line is hidden when no event has been received within the last 30 seconds. Each new event replaces the current text and resets the 30-second timer. Timestamp is local time, HH:mm.
Color mapping
Level is rendered via a WorkerLogLevelToBrushConverter (mirrors existing StatusColorConverter pattern):
| Level | Brush / color | Events |
|---|---|---|
Info |
TextDimBrush (dim) |
Created worktree, Started Claude, Committed changes |
Success |
#4CAF50 green |
Merged, Finished (done) |
Warn |
#FFA726 amber |
Discarded worktree, Reset |
Error |
#EF5350 red |
Finished (failed) |
Event Catalog
Seven emit sites. Each is added alongside the existing _logger.LogInformation(...) call — no log-sink plumbing, no central event bus.
| Site | Level | Message |
|---|---|---|
WorktreeManager.CreateAsync |
Info |
Created worktree for "<title>" |
WorktreeManager.DiscardAsync |
Warn |
Discarded worktree for "<title>" |
TaskMergeService.MergeAsync |
Success |
Merged "<title>" into <target> |
TaskResetService.ResetAsync |
Warn |
Reset "<title>" |
TaskRunner — Claude launch |
Info |
Started Claude for "<title>" |
TaskRunner — auto-commit |
Info |
Committed changes in "<title>" |
TaskRunner — task finished |
Success / Error |
Finished "<title>" (<status>) |
<title> is the task's display title; <target> is the merge target branch; <status> is done or failed.
Architecture
Shared contract (ClaudeDo.Data)
New enum:
namespace ClaudeDo.Data.Models;
public enum WorkerLogLevel
{
Info,
Success,
Warn,
Error,
}
SignalR is configured to serialize enums as strings via JsonStringEnumConverter (added to the hub's JSON options in Program.cs). The UI client deserializes back to the same enum.
Server side (ClaudeDo.Worker)
HubBroadcaster gets a new method:
public Task WorkerLog(string message, WorkerLogLevel level, DateTime timestampUtc) =>
_hub.Clients.All.SendAsync("WorkerLog", message, level, timestampUtc);
HubBroadcaster is already injected into TaskRunner. For WorktreeManager, TaskMergeService, and TaskResetService, add constructor injection where it isn't already present. Each emit site calls _broadcaster.WorkerLog(...) with DateTime.UtcNow next to the existing _logger.LogInformation(...).
Client side (ClaudeDo.Ui)
WorkerClient — register a HubConnection.On<string, WorkerLogLevel, DateTime>("WorkerLog", ...) handler and expose a WorkerLogReceived event with a small WorkerLogEntry(string Message, WorkerLogLevel Level, DateTime TimestampUtc) record.
Footer VM — StatusBarViewModel already exists; extend it (or introduce a small FooterViewModel if StatusBarViewModel turns out to be scoped elsewhere — confirm during implementation). Add:
[ObservableProperty] string? currentEventText[ObservableProperty] WorkerLogLevel currentEventLevel[ObservableProperty] bool isEventVisible- A
DispatcherTimerwith a 30-second interval. On eachWorkerLogReceived:- Format
HH:mm · <message>from the event's local time. - Set
CurrentEventText,CurrentEventLevel,IsEventVisible = true. - Stop and restart the timer.
- Format
- On timer tick:
IsEventVisible = false,CurrentEventText = null.
XAML — MainWindow.axaml footer StackPanel becomes a DockPanel. Existing ellipses + connection text dock left in a horizontal StackPanel. A new TextBlock docks right, bound to CurrentEventText with Foreground="{Binding CurrentEventLevel, Converter={StaticResource WorkerLogLevelToBrush}}", IsVisible="{Binding IsEventVisible}", and TextTrimming="CharacterEllipsis". Same mono font / size 10 as the rest of the footer.
Converter — WorkerLogLevelToBrushConverter in Converters/ returns a brush per enum value, resolving theme brushes via Application.Current.Resources for Info (to honor theme swaps) and hard-coding the success/warn/error hex values (those are already hard-coded in the current footer).
Testing
- Unit test
WorkerLogLevelToBrushConverterwith each enum value. - Unit test the footer VM: receiving an event sets text/level/visibility and schedules a clear; a second event within 30s replaces and resets the timer; after 30s of silence the line hides.
- Manual smoke: run a task end-to-end and confirm each of the seven events surfaces with the expected color and copy.
Edge Cases
- UI disconnected during an event: event is lost. Acceptable — reconnect resumes receiving new events.
- Burst of events: each replaces the previous; only the most recent is shown.
- Long task title: ellipsized by the TextBlock; connection pill on the left stays fully visible.
- Clock skew between Worker and UI: timestamp is formatted in UI's local time from the wire-format
DateTime(sent as UTC). Minor skew is cosmetic; no correctness impact.
Out of Scope / Future
- Click-to-expand history drawer
- Per-list or per-task event filtering
- Persisting the most recent N events across restarts