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.
128 lines
4.4 KiB
C#
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)..]);
|
|
}
|
|
}
|
|
}
|