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
@@ -101,6 +101,12 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
[ObservableProperty] private bool _isLetClaudeVisible;
[ObservableProperty] private bool _isQuickClaudeVisible;
// Shared by QueuePlanningSubtasksAsync and FinalizePlanningSessionAsync — both are triggered
// from a context menu that closes the instant a click lands, so there is no per-row surface
// left standing to anchor an indicator to. The island header is the nearest surface still
// visible after the menu closes.
public OperationStatus PlanningOperation { get; } = new();
public event EventHandler? LetClaudeHandleRequested;
[RelayCommand]
@@ -152,6 +158,15 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
Loc.LanguageChanged += _langChangedHandler;
_reconcileTimer.Elapsed += (_, _) => Dispatcher.UIThread.Post(() => _ = ReconcileTickAsync());
_reconcileTimer.Start();
// OperationStatus is a nested ObservableObject — [NotifyCanExecuteChangedFor] can't see
// into it, so IsRunning changes are relayed by hand.
PlanningOperation.PropertyChanged += (_, e) =>
{
if (e.PropertyName != nameof(OperationStatus.IsRunning)) return;
QueuePlanningSubtasksCommand.NotifyCanExecuteChanged();
FinalizePlanningSessionCommand.NotifyCanExecuteChanged();
};
}
public void Dispose()
@@ -1337,7 +1352,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
OpenPlanningConPtyRequested?.Invoke(row.Id, true);
break;
case UnfinishedPlanningModalResult.FinalizeNow:
await _worker.FinalizePlanningSessionAsync(row.Id, queueAgentTasks: false);
await FinalizePlanningSessionAsync(row);
break;
case UnfinishedPlanningModalResult.Discard:
await TryDiscardPlanningWithRetryAsync(row.Id);
@@ -1393,18 +1408,22 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
/// </summary>
public Func<string, Task<bool>>? ConfirmAsync { get; set; }
[RelayCommand]
private bool CanRunPlanningOperation() => !PlanningOperation.IsRunning;
[RelayCommand(CanExecute = nameof(CanRunPlanningOperation))]
private async Task QueuePlanningSubtasksAsync(TaskRowViewModel? row)
{
if (row is null || _worker is null) return;
using var op = PlanningOperation.Begin(Loc.T("ops.planning.queuingSubtasks"));
try { await _worker.QueuePlanningSubtasksAsync(row.Id); }
catch { }
}
[RelayCommand]
[RelayCommand(CanExecute = nameof(CanRunPlanningOperation))]
private async Task FinalizePlanningSessionAsync(TaskRowViewModel? row)
{
if (row is null) return;
using var op = PlanningOperation.Begin(Loc.T("ops.planning.finalizing"));
try { await _worker!.FinalizePlanningSessionAsync(row.Id, queueAgentTasks: false); }
catch { }
}