chore(claude-do): merge [A4] WeeklyReport, DailyPrep und Planning-Aktionen auf Opera

ClaudeDo-Task: 7c6f2f66-8cd7-4abb-8151-aed42ae739b5
This commit is contained in:
Mika Kuns
2026-08-12 10:10:57 +02:00
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 { }
}
@@ -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 { }
}