feat(claude-do): [A4] WeeklyReport, DailyPrep und Planning-Aktionen auf OperationStatus umstellen

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.
This commit is contained in:
Mika Kuns
2026-08-12 09:39:42 +02:00
parent d3abd4b88b
commit bbd6151ba6
9 changed files with 342 additions and 21 deletions
@@ -3,6 +3,7 @@ using System.Text;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Ui.Helpers;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
namespace ClaudeDo.Ui.ViewModels.Islands;
@@ -19,11 +20,23 @@ public sealed partial class PrepPanelViewModel : ViewModelBase, IDisposable
[ObservableProperty] private bool _isPrepRunning;
// Covers the gap between the click and the worker's PrepStartedEvent broadcast — IsPrepRunning
// stays false until that event arrives, so without this the button click looks like a no-op.
public OperationStatus PrepOperation { get; } = new();
public ObservableCollection<LogLineViewModel> PrepLog { get; } = new();
public bool ShowPrepEmptyState => !IsPrepRunning && PrepLog.Count == 0;
partial void OnIsPrepRunningChanged(bool value) => OnPropertyChanged(nameof(ShowPrepEmptyState));
// The long-running broadcast-driven state (IsPrepRunning) and the click-to-event gap
// (PrepOperation.IsRunning) both need to block a second click.
public bool IsPlanDayEnabled => !IsPrepRunning && !PrepOperation.IsRunning;
partial void OnIsPrepRunningChanged(bool value)
{
OnPropertyChanged(nameof(ShowPrepEmptyState));
OnPropertyChanged(nameof(IsPlanDayEnabled));
}
public PrepPanelViewModel(IWorkerClient worker)
{
@@ -37,6 +50,15 @@ public sealed partial class PrepPanelViewModel : ViewModelBase, IDisposable
_worker.PrepFinishedEvent += _onPrepFinishedHandler;
PrepLog.CollectionChanged += (_, _) => OnPropertyChanged(nameof(ShowPrepEmptyState));
// OperationStatus is a nested ObservableObject — [NotifyCanExecuteChangedFor] can't see
// into it, so IsRunning changes are relayed by hand.
PrepOperation.PropertyChanged += (_, e) =>
{
if (e.PropertyName != nameof(OperationStatus.IsRunning)) return;
PlanDayCommand.NotifyCanExecuteChanged();
OnPropertyChanged(nameof(IsPlanDayEnabled));
};
}
public void Dispose()
@@ -46,9 +68,12 @@ public sealed partial class PrepPanelViewModel : ViewModelBase, IDisposable
_worker.PrepFinishedEvent -= _onPrepFinishedHandler;
}
[RelayCommand]
private bool CanPlanDay() => IsPlanDayEnabled;
[RelayCommand(CanExecute = nameof(CanPlanDay))]
private async System.Threading.Tasks.Task PlanDayAsync()
{
using var op = PrepOperation.Begin(Loc.T("ops.reports.runningDailyPrep"));
try { await _worker.RunDailyPrepNowAsync(); }
catch { }
}