22 KiB
Daily Prep — Live Output View + Clear Day — 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: Stream the daily-prep run's output into a live, human-readable view (a new mode in the Details island), and add a "Clear Day" button that empties MyDay.
Architecture: The worker broadcasts PrepStarted/PrepLine/PrepFinished over SignalR (mirroring TaskStarted/TaskMessage/TaskFinished). PrimeRunner forwards each Claude stdout line instead of discarding it. The UI WorkerClient re-raises these as events; DetailsIslandViewModel gains a PrepLog + IsPrepMode panel rendered with the existing terminal renderer. A ClearMyDay hub method bulk-clears IsMyDay. MyDay header gets "Vorbereitungs-Log" and "Tag leeren" buttons.
Tech Stack: .NET 8, ASP.NET Core SignalR, EF Core (SQLite), Avalonia + CommunityToolkit.Mvvm, xUnit.
Spec: docs/superpowers/specs/2026-06-03-daily-prep-live-view-design.md
Build & test commands
.slnx needs .NET 9; build/test individual csproj with -c Release (a running Worker may lock Debug).
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
UI cannot be GUI-smoke-tested headlessly — note that explicitly where it applies; the human verifies visuals.
Reference anchors (verify before editing — line numbers drift)
src/ClaudeDo.Worker/Prime/Interfaces/IPrimeBroadcaster.cs— currently onlyPrimeFiredAsync.src/ClaudeDo.Worker/Hub/HubBroadcaster.cs:13-57— broadcast methods;PrimeFiredat ~52-56.src/ClaudeDo.Worker/Prime/PrimeRunner.cs:31-79—FireAsync; discard lambda at ~55-60; ctor at ~19-29.src/ClaudeDo.Worker/Hub/WorkerHub.cs:542-549—RunDailyPrepNow(uses_broadcaster); DailyNote CRUD at 559-583 (shows the db-context pattern this hub uses).src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs:19—TaskMessageEvent;:55—RunDailyPrepNowAsync.src/ClaudeDo.Ui/Services/WorkerClient.cs:99-122—TaskStarted/Finished/Messagehub.On;:170-173—PrimeFiredhub.On (the pattern to copy).src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs—IsNotesMode~56,Log~193, ctor/subscriptions ~272-337,OnTaskMessage~339-363 (stdout→StreamLineFormatter→Log),ShowNotes~478-483.src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml:131-302— body grid; task panelIsVisible="{Binding !IsNotesMode}", notes panelIsVisible="{Binding IsNotesMode}";SessionTerminalViewembedded ~295.src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml:54-75—ItemsControl ItemsSource="{Binding Log}"+ theLogLineViewModelitem template to reuse.src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs—NotesRequested~29,OpenNotesCommand+PrepareDayCommand~33-45,ShowNotesRow/IsMyDayList~65-66, both set inLoadForList~212-213.src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml:69-84— Notes + PrepareDay buttons (styling to copy).src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs:199-201— island event wiring;:225—PrimeFiredsubscription.- Fakes to keep in sync:
tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs,tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs(FakeWorkerClient).
Task 1: Worker — prep output broadcast + streaming
Files:
-
Modify:
src/ClaudeDo.Worker/Prime/Interfaces/IPrimeBroadcaster.cs -
Modify:
src/ClaudeDo.Worker/Hub/HubBroadcaster.cs -
Modify:
src/ClaudeDo.Worker/Prime/PrimeRunner.cs -
Test:
tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs -
Step 1: Write the failing test. Extend
PrimeRunnerTestswith a fakeIPrimeBroadcasterthat records calls. The fakeIClaudeProcessshould invokeonStdoutLinewith two sample lines and returnRunResult { ExitCode = 0, ResultMarkdown = "ok" }.
[Fact]
public async Task FireAsync_streams_started_lines_and_finished()
{
var broadcaster = new RecordingPrimeBroadcaster();
var claude = new FakeClaudeProcess(emitLines: new[] { "{\"a\":1}", "{\"b\":2}" }, exitCode: 0, result: "ok");
var runner = NewRunner(claude, broadcaster); // build with temp-sqlite dbFactory + fake clock + logger + broadcaster
var schedule = new PrimeScheduleDto(Guid.Empty, 0, TimeSpan.Zero, true, null, null);
var outcome = await runner.FireAsync(schedule, CancellationToken.None);
Assert.True(outcome.Success);
Assert.Equal(1, broadcaster.StartedCount);
Assert.Equal(new[] { "{\"a\":1}", "{\"b\":2}" }, broadcaster.Lines);
Assert.Single(broadcaster.FinishedResults);
Assert.True(broadcaster.FinishedResults[0]);
}
RecordingPrimeBroadcaster implements IPrimeBroadcaster: StartedCount, List<string> Lines, List<bool> FinishedResults, and a no-op PrimeFiredAsync. If the existing FakeClaudeProcess cannot emit lines, add an optional emitLines parameter that loops await onStdoutLine(line) before returning.
- Step 2: Run — expect FAIL (interface methods + ctor param missing).
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter PrimeRunner
- Step 3: Extend
IPrimeBroadcaster:
public interface IPrimeBroadcaster
{
Task PrimeFiredAsync(Guid scheduleId, bool success, string message, DateTimeOffset firedAt);
Task PrepStartedAsync();
Task PrepLineAsync(string line);
Task PrepFinishedAsync(bool success);
}
(Keep the existing PrimeFiredAsync signature exactly as it is in the current file.)
- Step 4: Implement in
HubBroadcaster(add next toPrimeFired):
public Task PrepStarted() => _hub.Clients.All.SendAsync("PrepStarted");
public Task PrepLine(string line) => _hub.Clients.All.SendAsync("PrepLine", line);
public Task PrepFinished(bool success) => _hub.Clients.All.SendAsync("PrepFinished", success);
Task IPrimeBroadcaster.PrepStartedAsync() => PrepStarted();
Task IPrimeBroadcaster.PrepLineAsync(string line) => PrepLine(line);
Task IPrimeBroadcaster.PrepFinishedAsync(bool success) => PrepFinished(success);
(Match the existing explicit-interface style used for PrimeFiredAsync.)
- Step 5: Wire
PrimeRunner. AddIPrimeBroadcaster _broadcasteras a ctor param (and field). Rewrite the body ofFireAsyncafter the gate check to:
if (!await _gate.WaitAsync(0, ct))
return new PrimeRunOutcome(false, "Daily prep already running");
var success = false;
try
{
await _broadcaster.PrepStartedAsync();
var cwd = Paths.AppDataRoot();
Directory.CreateDirectory(cwd);
int maxTasks;
await using (var dbCtx = await _dbFactory.CreateDbContextAsync(ct))
{
var settings = await new AppSettingsRepository(dbCtx).GetAsync(ct);
maxTasks = settings.DailyPrepMaxTasks < 1 ? 1 : settings.DailyPrepMaxTasks;
}
var today = DateOnly.FromDateTime(_clock.Now.LocalDateTime);
var prompt = DailyPrepPrompt.BuildPrompt(maxTasks, today);
var args = DailyPrepPrompt.BuildArgs(MaxTurns);
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeoutCts.CancelAfter(FireTimeout);
var result = await _claude.RunAsync(
arguments: args,
prompt: prompt,
workingDirectory: cwd,
onStdoutLine: line => _broadcaster.PrepLineAsync(line),
ct: timeoutCts.Token);
success = result.IsSuccess;
return success
? new PrimeRunOutcome(true, "Daily prep complete")
: new PrimeRunOutcome(false, $"exit code {result.ExitCode}");
}
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
{
return new PrimeRunOutcome(false, $"timed out after {FireTimeout.TotalMinutes:0} min");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Daily prep run failed");
return new PrimeRunOutcome(false, ex.Message);
}
finally
{
await _broadcaster.PrepFinishedAsync(success);
_gate.Release();
}
DI is unchanged: AddSingleton<IPrimeRunner, PrimeRunner>() resolves IPrimeBroadcaster (registered as sp => sp.GetRequiredService<HubBroadcaster>()).
- Step 6: Update existing
PrimeRunnerTestsctor calls to pass the recording broadcaster; build + run.
dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release --filter PrimeRunner
- Step 7: Commit.
git add src/ClaudeDo.Worker/Prime src/ClaudeDo.Worker/Hub/HubBroadcaster.cs tests/ClaudeDo.Worker.Tests/Prime
git commit -m "feat(daily-prep): stream prep output via PrepStarted/PrepLine/PrepFinished"
Task 2: Worker — ClearMyDay hub method
Files:
-
Modify:
src/ClaudeDo.Worker/Hub/WorkerHub.cs -
Test: a new/existing hub test under
tests/ClaudeDo.Worker.Tests/Hub/(mirror an existing hub test that seeds a real SQLite db and constructsWorkerHub) -
Step 1: Write the failing test. Seed three tasks: two with
IsMyDay=true(one Idle, one Done), one withIsMyDay=false. ConstructWorkerHubthe way existing hub tests do (the samenull!argument list, plus a recordingHubBroadcaster/clients). CallClearMyDay(); assert both MyDay rows are nowfalse, the third is untouched, and the returned count is 2.
[Fact]
public async Task ClearMyDay_clears_all_isMyDay_tasks()
{
// seed via the test's db helper ...
var hub = NewHub(/* ... */);
var cleared = await hub.ClearMyDay();
Assert.Equal(2, cleared);
await using var ctx = NewContext();
Assert.False(await ctx.Tasks.AnyAsync(t => t.IsMyDay));
}
-
Step 2: Run — expect FAIL.
-
Step 3: Add the method to
WorkerHub(use the same db-context acquisition the neighbouring hub methods use — e.g._dbFactory/repository field name found in the file — and the existing_broadcasterfield):
public async Task<int> ClearMyDay()
{
await using var ctx = await _dbFactory.CreateDbContextAsync();
var ids = await ctx.Tasks.Where(t => t.IsMyDay).Select(t => t.Id).ToListAsync();
if (ids.Count == 0) return 0;
await ctx.Tasks.Where(t => t.IsMyDay)
.ExecuteUpdateAsync(s => s.SetProperty(t => t.IsMyDay, false));
foreach (var id in ids)
await _broadcaster.TaskUpdated(id);
return ids.Count;
}
If WorkerHub does not already have an IDbContextFactory<ClaudeDoDbContext> field, use whatever data-access dependency the other hub methods use (read the file). Do NOT add a new ctor param unless unavoidable (it would break hub-test fakes — if you must, update all new WorkerHub(...) call sites).
-
Step 4: Run — expect PASS. Build Worker.
-
Step 5: Commit.
git add src/ClaudeDo.Worker/Hub/WorkerHub.cs tests/ClaudeDo.Worker.Tests/Hub
git commit -m "feat(daily-prep): add ClearMyDay hub method"
Task 3: UI — WorkerClient prep events + ClearMyDayAsync
Files:
-
Modify:
src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs -
Modify:
src/ClaudeDo.Ui/Services/WorkerClient.cs -
Modify fakes:
tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs,tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs(FakeWorkerClient) -
Step 1: Declare on
IWorkerClient(nearTaskMessageEvent/RunDailyPrepNowAsync):
event Action? PrepStartedEvent;
event Action<string>? PrepLineEvent;
event Action<bool>? PrepFinishedEvent;
Task ClearMyDayAsync();
- Step 2: Implement in
WorkerClient. Add the events; register hub callbacks mirroring thePrimeFiredregistration (~line 170):
public event Action? PrepStartedEvent;
public event Action<string>? PrepLineEvent;
public event Action<bool>? PrepFinishedEvent;
// in the hub-wiring section:
_hub.On("PrepStarted", () => Dispatcher.UIThread.Post(() => PrepStartedEvent?.Invoke()));
_hub.On<string>("PrepLine", line => Dispatcher.UIThread.Post(() => PrepLineEvent?.Invoke(line)));
_hub.On<bool>("PrepFinished", ok => Dispatcher.UIThread.Post(() => PrepFinishedEvent?.Invoke(ok)));
public Task ClearMyDayAsync() => _connection.InvokeAsync("ClearMyDay");
(Use the exact connection field name and async-call style of neighbouring methods like RunDailyPrepNowAsync / GenerateWeekReport. ClearMyDay returns int on the hub; invoking it as a void InvokeAsync("ClearMyDay") is fine, or InvokeAsync<int> if you want the count.)
-
Step 3: Update the fakes. Add the three events (as
public event …auto-implemented) andClearMyDayAsync() => Task.CompletedTaskto bothStubWorkerClientandFakeWorkerClient. For the ClearDay command test (Task 5), giveStubWorkerClientaClearMyDayCallscounter incremented inClearMyDayAsync. -
Step 4: Build App + both test projects; fix any remaining fake gaps.
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
- Step 5: Commit.
git add src/ClaudeDo.Ui/Services tests
git commit -m "feat(daily-prep): expose prep stream events and ClearMyDay on the UI worker client"
Task 4: UI — Details island prep mode + live log
Files:
-
Modify:
src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs -
Modify:
src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml -
Test:
tests/ClaudeDo.Ui.Tests/...DetailsIslandViewModel...(mirror existing Details VM tests; if none, add a small test file) -
Step 1: Write the failing test. Construct
DetailsIslandViewModelwith aStubWorkerClient(mirror existing construction). Then:
[Fact]
public void PrepLine_event_appends_to_PrepLog()
{
var stub = new StubWorkerClient();
var vm = NewDetailsVm(stub);
stub.RaisePrepLine("{\"type\":\"assistant\",\"text\":\"hi\"}"); // helper that invokes PrepLineEvent
Assert.NotEmpty(vm.PrepLog);
}
[Fact]
public void ShowPrep_sets_prep_mode_and_clears_notes_mode()
{
var vm = NewDetailsVm(new StubWorkerClient());
vm.ShowPrep();
Assert.True(vm.IsPrepMode);
Assert.False(vm.IsNotesMode);
}
Add RaisePrepStarted/RaisePrepLine/RaisePrepFinished helpers to StubWorkerClient that invoke the corresponding events.
-
Step 2: Run — expect FAIL.
-
Step 3: Implement in
DetailsIslandViewModel:- Add
[ObservableProperty] private bool _isPrepMode;and[ObservableProperty] private bool _isPrepRunning;. - Add
public ObservableCollection<LogLineViewModel> PrepLog { get; } = new();. - In the ctor, subscribe:
_worker.PrepStartedEvent += OnPrepStarted; _worker.PrepLineEvent += OnPrepLine; _worker.PrepFinishedEvent += OnPrepFinished;(guard with the same_worker is not nullpattern used for other events). - Handlers:
- Add
private void OnPrepStarted()
{
PrepLog.Clear();
IsPrepRunning = true;
}
private void OnPrepLine(string line) => AppendStdoutLine(PrepLog, line);
private void OnPrepFinished(bool success) => IsPrepRunning = false;
-
Factor the stdout-formatting currently inside
OnTaskMessageinto a reusableprivate void AppendStdoutLine(ObservableCollection<LogLineViewModel> target, string line)that runs the line throughStreamLineFormatterand appendsLogLineViewModel(s). HaveOnTaskMessage's stdout branch callAppendStdoutLine(Log, strippedLine)so both paths share one implementation. (Events arrive already on the UI thread viaDispatcher.UIThread.PostinWorkerClient, so direct collection mutation is correct.) -
Add
public void ShowPrep()mirroringShowNotes(): callBind(null), setIsNotesMode = false,IsPrepMode = true. -
In
ShowNotes()addIsPrepMode = false. InBind(...)reset bothIsNotesModeandIsPrepModeto false (find whereIsNotesModeis reset; addIsPrepModebeside it). -
Step 4: Update
DetailsIslandView.axaml.- Change the task-details panel visibility from
IsVisible="{Binding !IsNotesMode}"to a converter-free multi-condition. Avalonia lacks&&in bindings, so add a computed propertypublic bool IsTaskDetailVisible => !IsNotesMode && !IsPrepMode;to the VM (raise its change notification from theOnIsNotesModeChanged/OnIsPrepModeChangedpartial methods generated by[ObservableProperty]) and bind the task panel toIsVisible="{Binding IsTaskDetailVisible}". - Add a third panel after the notes panel:
- Change the task-details panel visibility from
<Panel IsVisible="{Binding IsPrepMode}">
<DockPanel>
<TextBlock DockPanel.Dock="Top" Margin="16,12"
Text="{loc:Tr details.prepTitle}" Classes="h2"/>
<ScrollViewer>
<ItemsControl ItemsSource="{Binding PrepLog}"/>
</ScrollViewer>
</DockPanel>
</Panel>
The ItemsControl reuses the implicit LogLineViewModel DataTemplate that
SessionTerminalView relies on. If that template is defined locally inside
SessionTerminalView.axaml (not in a shared resource), either move it to a shared
ResourceDictionary (e.g. App resources) and reference it from both, or set the
ItemsControl.ItemTemplate to a copy of that template. Prefer sharing over copying.
Add details.prepTitle ("Daily prep" / "Tagesvorbereitung") to both locale json files.
- Step 5: Run UI tests — expect PASS; build App.
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
- Step 6: Commit.
git add src/ClaudeDo.Ui src/ClaudeDo.Localization tests/ClaudeDo.Ui.Tests
git commit -m "feat(daily-prep): add live prep-output mode to the Details island"
Task 5: UI — MyDay buttons + shell wiring
Files:
-
Modify:
src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs -
Modify:
src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml -
Modify:
src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs -
Modify:
src/ClaudeDo.Localization/locales/en.json,de.json -
Test:
tests/ClaudeDo.Worker.Tests/UiVm/...ortests/ClaudeDo.Ui.Tests/...(TasksIslandViewModel) -
Step 1: Write the failing tests.
[Fact]
public async Task ClearDayCommand_calls_worker()
{
var stub = new StubWorkerClient();
var vm = NewTasksVm(stub);
await vm.ClearDayCommand.ExecuteAsync(null);
Assert.Equal(1, stub.ClearMyDayCalls);
}
[Fact]
public async Task PrepareDayCommand_raises_PrepRequested()
{
var vm = NewTasksVm(new StubWorkerClient());
var raised = false;
vm.PrepRequested += () => raised = true;
await vm.PrepareDayCommand.ExecuteAsync(null);
Assert.True(raised);
}
-
Step 2: Run — expect FAIL.
-
Step 3: Implement in
TasksIslandViewModel:- Add
public event Action? PrepRequested;next toNotesRequested. - In
PrepareDayAsync(the existing[RelayCommand]), raisePrepRequested?.Invoke();in addition to the existingRunDailyPrepNowAsync()call. - Add:
- Add
[RelayCommand]
private void ShowPrepLog() => PrepRequested?.Invoke();
[RelayCommand]
private async Task ClearDayAsync()
{
if (_worker is null) return;
try { await _worker.ClearMyDayAsync(); }
catch { /* worker offline; broadcast will reconcile on return */ }
}
- Step 4: Add the two buttons to the MyDay header in
TasksIslandView.axaml, immediately after the existing "Prepare day" button (~line 84), copying its styling (DockPanel.Dock="Top" Classes="btn" HorizontalAlignment="Stretch" HorizontalContentAlignment="Left" Margin="16,0,16,8" IsVisible="{Binding IsMyDayList}"):
<Button DockPanel.Dock="Top" Classes="btn" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left" Margin="16,0,16,8"
IsVisible="{Binding IsMyDayList}"
Command="{Binding ShowPrepLogCommand}"
Content="{loc:Tr tasks.prepLog}"/>
<Button DockPanel.Dock="Top" Classes="btn" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left" Margin="16,0,16,8"
IsVisible="{Binding IsMyDayList}"
Command="{Binding ClearDayCommand}"
Content="{loc:Tr tasks.clearDay}"/>
Add tasks.prepLog (en "Prep log" / de "Vorbereitungs-Log") and tasks.clearDay
(en "Clear day" / de "Tag leeren") to both locale json files.
- Step 5: Wire the shell. In
IslandsShellViewModelwhereTasks.NotesRequestedis wired (~line 201), add:
Tasks.PrepRequested += () => Details.ShowPrep();
- Step 6: Run tests + build App.
dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release
dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release
-
Step 7: Manual smoke (human, not headless): start Worker + App, open MyDay, click "Tag vorbereiten" → Details island opens in prep mode and streams readable lines; click "Tag leeren" → MyDay empties; after a scheduled run, "Vorbereitungs-Log" opens the filled log. Confirm the three buttons only appear on MyDay.
-
Step 8: Commit.
git add src/ClaudeDo.Ui src/ClaudeDo.Localization tests
git commit -m "feat(daily-prep): add Prep-log and Clear-day buttons to MyDay header"
Final verification
- Build Worker + App (Release).
dotnet testWorker.Tests, Ui.Tests, Localization.Tests — all green.- Manual: prep streams live into the Details island (manual opens it; scheduled fills it silently, opened via the button); Clear Day empties MyDay immediately.
Notes / risks
- Mode flags
IsNotesMode/IsPrepModeare mutually exclusive; the task-details panel uses the computedIsTaskDetailVisible. Verify all three modes switch cleanly. - Reusing the
LogLineViewModeltemplate: prefer promoting it to a shared resource over copying, to avoid drift between the session terminal and the prep log. ClearMyDaybroadcasts oneTaskUpdatedper affected id; MyDay is small (capped), so this is fine.- Keep
PrimeRunner's "already running" early-return emitting no prep events.