122 lines
6.6 KiB
Markdown
122 lines
6.6 KiB
Markdown
# 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
|