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 _onPrepLineHandler; private readonly Action _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 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)..]); } } }