From da19eb807bba518e3792edb16bdfee3e760db222 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 23 Apr 2026 13:56:09 +0200 Subject: [PATCH] docs(superpowers): add worker-log footer design spec --- .../2026-04-23-worker-log-footer-design.md | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-23-worker-log-footer-design.md diff --git a/docs/superpowers/specs/2026-04-23-worker-log-footer-design.md b/docs/superpowers/specs/2026-04-23-worker-log-footer-design.md new file mode 100644 index 0000000..e4f8b90 --- /dev/null +++ b/docs/superpowers/specs/2026-04-23-worker-log-footer-design.md @@ -0,0 +1,121 @@ +# 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 · `, 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 ""` | +| `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