Files
ClaudeDo/src/ClaudeDo.Ui/ViewModels/Islands/PrepPanelViewModel.cs
T
Mika Kuns bbd6151ba6 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.
2026-08-12 09:39:42 +02:00

128 lines
4.4 KiB
C#

using System.Collections.ObjectModel;
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;
public sealed partial class PrepPanelViewModel : ViewModelBase, IDisposable
{
private readonly IWorkerClient _worker;
private readonly StreamLineFormatter _formatter = new();
private readonly StringBuilder _prepClaudeBuf = new();
private readonly Action _onPrepStartedHandler;
private readonly Action<string> _onPrepLineHandler;
private readonly Action<bool> _onPrepFinishedHandler;
[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;
// 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)
{
_worker = worker;
_onPrepStartedHandler = OnPrepStarted;
_onPrepLineHandler = OnPrepLine;
_onPrepFinishedHandler = OnPrepFinished;
_worker.PrepStartedEvent += _onPrepStartedHandler;
_worker.PrepLineEvent += _onPrepLineHandler;
_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()
{
_worker.PrepStartedEvent -= _onPrepStartedHandler;
_worker.PrepLineEvent -= _onPrepLineHandler;
_worker.PrepFinishedEvent -= _onPrepFinishedHandler;
}
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 { }
}
public async System.Threading.Tasks.Task LoadLastPrepLogIfEmptyAsync()
{
if (IsPrepRunning || PrepLog.Count > 0) return;
string text;
try { text = await _worker.GetLastPrepLogAsync(); }
catch { return; }
if (IsPrepRunning || PrepLog.Count > 0) return;
foreach (var line in text.Split('\n'))
{
var trimmed = line.TrimEnd('\r');
if (trimmed.Length > 0) AppendStdoutLine(trimmed);
}
}
private void OnPrepStarted()
{
PrepLog.Clear();
IsPrepRunning = true;
}
private void OnPrepLine(string line) => AppendStdoutLine(line);
private void OnPrepFinished(bool success) => IsPrepRunning = false;
private void AppendStdoutLine(string line)
{
var formatted = _formatter.FormatLine(line);
if (formatted is null) return;
AppendClaudeText(formatted);
}
private void AppendClaudeText(string chunk)
{
_prepClaudeBuf.Append(chunk);
while (true)
{
var text = _prepClaudeBuf.ToString();
var nl = text.IndexOf('\n');
if (nl < 0) break;
var piece = text[..nl].TrimEnd('\r');
if (!string.IsNullOrWhiteSpace(piece))
PrepLog.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
_prepClaudeBuf.Clear();
_prepClaudeBuf.Append(text[(nl + 1)..]);
}
}
}