# 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/RECONNECTING` text). The static `· WORKER` label 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:

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

```csharp
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 `DispatcherTimer` with a 30-second interval. On each `WorkerLogReceived`:
  1. Format `HH:mm · <message>` from the event's local time.
  2. Set `CurrentEventText`, `CurrentEventLevel`, `IsEventVisible = true`.
  3. Stop and restart the timer.
- 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 `WorkerLogLevelToBrushConverter` with 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
