IsTasksEmptyRepoHintVisible reads _currentList?.Kind directly, but Kind
isn't itself observed — the NotifyPropertyChangedFor chain only fires
when IsLetClaudeVisible's value actually changes. Switching from a
Smart list to an empty User list without a WorkingDir changes Kind
while IsLetClaudeVisible (and Has*) stay false in both, so the hint
never notified and stayed stale.
Zentrierter, gedimmter Platzhalter (Mission-Control-Muster) im
Detail-Pane bei fehlender Selektion (nicht in Notes-/Prep-Modus) und
in der Task-Liste bei 0 sichtbaren Tasks; User-Listen ohne
WorkingDir bekommen zusätzlich den Repo-Verknüpfungs-Hinweis.
Sechs Konsistenz-Fixes: Delete-Task ins Zeilen-Kontextmenü (routet über den
Worker wie DetailsIslandViewModel.DeleteTaskAsync, damit ein gelöschtes Child
den WaitingForChildren-Parent korrekt weiterschaltet); EnqueueCommand als
"Send to queue"-Button im Detail-Pane verdrahtet; Settings-Eintrag im
Listen-Kontextmenü ergänzt; Worktree-Discard und Reset-All laufen jetzt über
das jeweils vorhandene Confirm-Hook (ConfirmAction / neuer ConfirmAsync-Hook
auf WorktreesSettingsTabViewModel) statt ohne Rückfrage bzw. über ein
Inline-Reveal-Banner; killSessionTip/closeSession und die deutschen
Queue-Strings im usageMonitor vereinheitlicht.
Kein zweiter Dialog-Mechanismus eingeführt — überall die vorhandenen Hooks
wiederverwendet.
Cancel now shows an amber hint + tooltip while a merge drain is in
progress (ShowMergeDrainHint mirrors IsMergeDraining). Continue,
Reset & Retry, Open Diff, Worktree, and Review Combined Diff gain
ToolTip.Tip + ToolTip.ShowOnDisabled explaining why they're locked.
No CanExecute or behavior changed, only added communication.
Stop/Enqueue/Dequeue/Reset&Retry (DetailsIslandViewModel), status/cancel/reject
commands (TasksIslandViewModel), Mission Control's drag-enqueue and queue
refresh, and "Open findings folder" (ListsIslandViewModel) used to catch {}
or silently return on a blocked precondition. They now report through the
existing ErrorReported -> FlashFooterError path, with new en/de locale keys
and Ui.Tests covering each converted command.
A child merge that came back blocked/verify_failed/untracked_collision during a
parent/children unit merge used to vanish: DrainAsync only logged it server-side,
PlanningMergeAborted carried no reason, and ApproveReview/review_task always
reported StatusMerged for a task with children regardless of the real outcome,
so a failed unit merge left the parent stuck with no visible error.
- PlanningMergeOrchestrator.StartAsync/ContinueAsync/DrainAsync now return a
PlanningMergeResult(Status, Reason) instead of void, and PlanningMergeAborted
carries that reason to the UI.
- WorkerHub.ApproveReview and ExternalMcpService.ReviewTask's approve branch
propagate the real status/reason for a parent with children instead of
hardcoding "merged" (or masking a non-conflict failure as "conflict").
- StartAsync now requires the parent to already be WaitingForReview for
improvement parents too, not just planning ones, so a stale caller can no
longer trigger a partial child merge.
- HasActiveMerge now also covers the window between the last child merging and
FinalizeParentDoneAsync completing, closing a gap where a concurrent Cancel
could race the parent's own approve-to-Done transition.
- IslandsShellViewModel.OnPlanningMergeAborted flashes the reason via
FlashFooterError instead of only clearing the external-merge banner.
TaskRunner.ContinueAsync: Claim, SeedAsync und SetupMcpConfigAsync liefen
vor dem try/catch (anders als RunAsync) - warf einer davon nach dem
Running-Claim, propagierte die Exception ungefangen bis zu
OverrideSlotService.RunContinueInSlotAsync, das nur loggt. Task blieb
Running. Fix: derselbe Aufbau wie RunAsync (Claim+Seed+MCP-Setup im
try, MarkFailed im catch, mcpToken/mcpConfigPath vor dem try auf null).
SetupMcpConfigAsync bekommt zusätzlich einen onTokenRegistered-Callback,
damit die äußere mcpToken-Variable den Token sofort nach dem Register
sieht - sonst hätte ein Fehler zwischen Register und Rückgabe (z.B.
File.WriteAllTextAsync) den Token in der TaskRunTokenRegistry geleakt
(betrifft denselben Aufruf in RunAsync mit, daher dort ebenfalls
verdrahtet - RunAsync-Struktur selbst unverändert).
QueueService.RunInSlotAsync: der Stop-Button (CancelTask) cancelt die
Slot-CTS direkt ohne TaskStateService-Schreibzugriff. Traf das die
Pre-Dispatch-DB-Reads, loggte der OCE-Catch nur und die vom Picker
bereits auf Running geclaimte Task blieb dort für immer hängen. Fix:
Status im Catch neu lesen und nur wenn er noch Running ist über
TaskStateService.CancelAsync auf Cancelled setzen - ein Ursprung, der
bereits selbst einen Terminalstatus geschrieben hat (z.B. CancelReview),
wird nicht überschrieben. Kommentar korrigiert.
TDD: neue Tests in ContinueAsyncExceptionTests (Seed-/MCP-Setup-Fehler
nach Claim -> Failed, kein Token-Leak) und QueueServiceSlotFailureTests
(Stop während Pre-Dispatch -> Cancelled statt Running; ein bereits
terminal geschriebener Status wird nicht gestompt) vorher rot, jetzt
grün. Worker.Tests: 1213/1213 grün, Worker baut in Release.
Plan checkboxes stood at zero although P0, A, B, C1 and D are all merged --
anyone reading it would have assumed nothing was built. Ticks the merged
packages, records A5's cancellation and the visual pass, and names the two
leftovers (the three unused ops.* keys from A3/A4, C2-C5 as the only open
group). Also corrects the batch_* tool count from eight to seven and puts the
OperationTiming kill-switch note in the Ui CLAUDE.md back into English.
continue_merge and the parent/children unit-merge drain (PlanningMergeOrchestrator)
re-run the post-merge verify gate but never forwarded their IProgress token into it,
so a slow verify command on either path went silent past Claude Code's 300s MCP
idle-abort even though D1-D3 already fixed this for merge_task/review_task's
childless path. list_worktrees also gets elapsed-time progress: many tracked
worktrees means many concurrent git subprocess spawns.
Worker CLAUDE.md's existing progress rule now points at ProgressReporter as the
one implementation instead of a fresh polling loop.
Wraps the single-element git/worktree long-runners in ProgressReporter.RunAsync
(time-based, ExternalMcpService.ProgressReportInterval) and adds i/n reporting
to preview_merge_set's per-task loop, so these MCP calls survive Claude Code's
~300s idle-silence abort instead of leaving the caller with no signal that work
is still happening.
Each batch tool now sends an i/n progress ping via ProgressReporter.ReportItem
after processing every item, so a waiting agent doesn't see the MCP client's
300s idle-silence abort while the worker keeps looping. Covers BatchGetTasks,
BatchAddTasks, BatchUpdateTaskStatus, BatchCancelTasks, BatchDeleteTasks,
BatchSetMyDay, and BatchCleanupTaskWorktrees (the slowest of the seven, since
it does git work per task).
Pulls the MCP idle-timeout progress loop out of TaskMergeService into a
standalone ProgressReporter (Lifecycle namespace) shared by the verify
gate and preview-verify call sites, and adds a per-item i/n overload
(ReportItem) for upcoming batch progress in D2/D3.
Its job (surfacing the DetailsIsland.BindAsync churn) is done; leave the
InvokeTimedAsync wrappers and Island Record call sites in place as a
chokepoint for next time, but stop writing by default. Shared now only
writes when CLAUDEDO_OP_TIMING=1 is set; the constructor keeps enabled=true
so both TestSetup redirects and OperationTimingTests are unaffected.
DetailsIsland.BindAsync churn (~1900 binds/day, 91-96% cancelled) was not a
UI reactivity bug: OperationTiming.Shared is a static singleton hardcoded to
~/.todo-app/logs/operation-timing.ndjson, and Ui.Tests/Worker.Tests both
construct real DetailsIslandViewModel/TasksIslandViewModel instances that
call Shared.Record directly. Every dotnet test run appended 50-100 lines
straight into the live app's log — pid-burst analysis showed 13 distinct
test-run pids in the ":?" bucket (tests never pass a source) plus a
no-suffix bucket with no pid field at all (pre-dating the pid-per-line
feature). Real user-driven binds that day: 4.
Made Shared settable and added a [ModuleInitializer]-based TestSetup in
both test projects that redirects it to a per-process temp file before any
test runs.
Merge/verify phases now broadcast over a generic (opKey, phase, current,
total) Hub event instead of a merge-specific one, so future producers
(worktree cleanup, startup recovery, planning integration) can reuse it.
IWorkerClient.MergeProgressEvent stays as a thin forwarder for existing
consumers (elapsed seconds riding in the generic "current" slot).
Adds DiffDispatcherGuardTests, the one blockade-regression test in the
operation-feedback effort. Uses a queueing SynchronizationContext (Option B —
this project has no bootstrapped Avalonia dispatcher anywhere) to prove
DiffViewerViewModel's UnifiedDiffParser.Parse offload ([B1]) truly defers past
the calling thread rather than racing on timing. [B2]'s DiffAlignment.Build
offload in DiffTextView is deliberately not covered — three escalating
attempts to construct that control safely in-process each broke under the
full test suite (dispatcher thread-affinity crashes, then process-wide
Application state polluting an unrelated test), and Avalonia.Headless.XUnit
collides with this project's xUnit v2 stack. Verified the test fails against
the pre-[B1] code and passes with it; full suite green aside from a
pre-existing, unrelated flaky test.
DiffTextView.ReloadFile ran DiffAlignment.Build synchronously for both the
constructor and the File-property path, so a large diff (or several at once
in Planning mode's per-file ItemsControl) could freeze the UI thread.
Diffs at or under 500 raw lines still build inline (no placeholder frame);
larger ones build via Task.Run and a generation counter discards the result
if File changed again before the build finished.
Three causes behind the stuck indicator:
- OperationStatus.End did not retire the generation, so tick work posted
while the dispatcher was blocked drained afterwards and set
ShowIndicator back to true on a finished operation.
- The startup update check ran in Task.Run; OperationStatus writes its
observable properties on the calling thread, so the final
ShowIndicator=false never reached the binding.
- OperationIndicator overwrote its own DataContext from the Status
property, which re-targeted the call-site binding. It now scopes the
DataContext to an inner panel and collapses itself when Status is
null; every call site switched from DataContext= to Status=.
Also gates the footer connection pill in the command body instead of
CanExecute (a disabled command greyed out the ONLINE chip), and extends
OperationTiming: pid per line, 4 MB rollover, fast successes dropped
(failures always kept), and DetailsIsland.BindAsync now carries the
selection trigger via TasksIslandViewModel.SelectFrom.
ParseOp only covered LoadFilesAsync; OnDisplayedDiffChanged's parse (driven
by ToggleCombinedAsync and OnSelectedSubtaskChanged) had no visible
feedback even though the offload was already correct. Both call sites now
go through one ParseOffUiThreadAsync helper (Begin on the UI thread before
Task.Run, End via `using` regardless of a stale generation), so the
already-visible toolbar indicator covers the planning path without
duplicating the begin/offload/end ceremony.
Both call sites (LoadFilesAsync, OnDisplayedDiffChanged) ran the parser
synchronously on the dispatcher, freezing the window on a large diff. The
Files-mode parse+tree-build now runs via Task.Run behind a ParseOp
OperationStatus wired to the toolbar; the planning-mode parse is
fire-and-forget with a generation counter so a fast second DisplayedDiff
change can't write stale PlanningFiles.
The footer pill fired OpenWorkerConnectionHelpCommand unconditionally, so
clicking it on a connected worker showed the "WORKER NOT REACHABLE" dialog.
Gate the command on !IsConnected (not IsOffline — the retry loop stays in
"connecting" forever while the worker is down, which is exactly when the
dialog's "Start Worker" is needed) and re-evaluate CanExecute on connection
state changes.
Vorgaben: `docs/superpowers/plans/2026-08-11-operation-feedback.md`, Gruppe A, Entwurf A3. P0-1 ist gemergt: `src/ClaudeDo.Ui/Services/OperationStatus.cs` + `src/ClaudeDo.Ui/Views/Controls/OperationIndicator.axaml`.
SCOPE-ÄNDERUNG (Nutzer, 2026-08-12): OnlineInbox Sign-In/Sign-Out ist AUS DEM SCOPE GENOMMEN — vorerst unwichtig. `ViewModels/Modals/Settings/OnlineInboxSettingsViewModel.cs` und die
ClaudeDo-Task: 2356806f-f16e-4652-b615-4562e95c3a65
Approve, Submit, Reject, and Park each get their own OperationStatus so the
button locks and shows an OperationIndicator for the duration of the call;
Approve additionally scopes a MergeProgressEvent subscription to its own
task id to sharpen the label from "merging" to "verifying" mid-flight.
MergeSectionViewModel's preview refresh gets the same treatment for display
only, since it has no command button to gate.
WeeklyReportModalViewModel.Generate, PrepPanelViewModel.PlanDayAsync und die beiden
Planning-Aktionen (QueuePlanningSubtasksAsync, FinalizePlanningSessionAsync) laufen jetzt
durch je eine OperationStatus + OperationIndicator statt handgebauter Spinner. Die beiden
Planning-Aktionen teilen sich eine Instanz, angezeigt im Tasks-Island-Header, weil sie aus
einem sofort schliessenden Kontextmenue ausgeloest werden und es keine dauerhafte
Pro-Zeilen-Flaeche gibt, an die ein Indikator gehaengt werden koennte.
Replaces the shared IsBusy/IsMerging flags in WorktreesOverviewModalViewModel and the
reset flow in WorktreesSettingsTabViewModel with dedicated OperationStatus instances
shown via OperationIndicator, so a running Refresh no longer blocks the Cleanup
indicator. ForceRemove gains a CanExecute guard against re-entrancy while it's running,
and the reconcile tick's busy guard now checks all four action statuses instead of the
old IsBusy||IsMerging pair.