using System.Collections.Generic; using CommunityToolkit.Mvvm.ComponentModel; using ClaudeDo.Data.Models; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Ui.ViewModels.Islands; public sealed partial class TaskRowViewModel : ViewModelBase { public required string Id { get; init; } [ObservableProperty] private string _title = ""; [ObservableProperty] private string _listName = ""; [ObservableProperty] private bool _done; [ObservableProperty] private bool _isStarred; [ObservableProperty] private bool _isMyDay; [ObservableProperty] private bool _isSelected; [ObservableProperty] private TaskStatus _status; [ObservableProperty] private string? _branch; [ObservableProperty] private string? _diffStat; [ObservableProperty] private string? _liveTail; [ObservableProperty] private DateTime? _scheduledFor; [ObservableProperty] private int _diffAdditions; [ObservableProperty] private int _diffDeletions; [ObservableProperty] private bool _dropHintAbove; [ObservableProperty] private bool _dropHintBelow; [ObservableProperty] private string? _parentTaskId; [ObservableProperty] private string? _blockedByTaskId; [ObservableProperty] private bool _isExpanded = true; [ObservableProperty] private bool _hasPlanningChildren; [ObservableProperty] private bool _hasQueuedSubtasks; public DateTime CreatedAt { get; init; } public string CreatedAtFormatted => CreatedAt == default ? "—" : $"Created {CreatedAt:MMM d}"; public IReadOnlyList Tags { get; init; } = Array.Empty(); public int StepsCount { get; init; } public int StepsCompleted { get; init; } public bool IsChild => !string.IsNullOrEmpty(ParentTaskId); public bool IsPlanningParent => Status == TaskStatus.Planning || Status == TaskStatus.Planned || HasPlanningChildren; public bool IsDraft => Status == TaskStatus.Draft; public bool CanOpenPlanningSession => Status == TaskStatus.Manual && !IsChild; public bool CanResumeOrDiscardPlanning => Status == TaskStatus.Planning; public string? PlanningBadge => Status switch { TaskStatus.Planning => "PLANNING", TaskStatus.Planned => "PLANNED", _ => null, }; public bool HasBranch => !string.IsNullOrWhiteSpace(Branch); public bool HasDiff => DiffAdditions > 0 || DiffDeletions > 0; public bool HasTags => Tags.Count > 0; public bool HasSteps => StepsCount > 0; public bool IsOverdue => ScheduledFor is { } d && d.Date < DateTime.Today && !Done; public bool IsRunning => Status == TaskStatus.Running; public bool IsQueued => Status == TaskStatus.Queued && string.IsNullOrEmpty(BlockedByTaskId); public bool IsWaiting => (Status == TaskStatus.Queued && !string.IsNullOrEmpty(BlockedByTaskId)) || Status == TaskStatus.Waiting; public bool CanRemoveFromQueue => IsQueued || HasQueuedSubtasks; public bool HasSchedule => ScheduledFor.HasValue; public bool HasLiveTail => IsRunning && !string.IsNullOrEmpty(LiveTail); public string DiffAdditionsText => $"+{DiffAdditions}"; public string DiffDeletionsText => $"−{DiffDeletions}"; public string StepsText => $"{StepsCompleted}/{StepsCount} steps"; public string StatusChipClass => (Status, IsBlocked: !string.IsNullOrEmpty(BlockedByTaskId)) switch { (TaskStatus.Running, _) => "running", (TaskStatus.Failed, _) => "error", (TaskStatus.Done, _) => "review", (TaskStatus.Queued, true) => "waiting", (TaskStatus.Queued, false) => "queued", (TaskStatus.Waiting, _) => "waiting", _ => "idle", }; partial void OnStatusChanged(TaskStatus value) { OnPropertyChanged(nameof(StatusChipClass)); OnPropertyChanged(nameof(IsRunning)); OnPropertyChanged(nameof(IsQueued)); OnPropertyChanged(nameof(IsWaiting)); OnPropertyChanged(nameof(HasLiveTail)); OnPropertyChanged(nameof(IsPlanningParent)); OnPropertyChanged(nameof(PlanningBadge)); OnPropertyChanged(nameof(IsDraft)); OnPropertyChanged(nameof(CanOpenPlanningSession)); OnPropertyChanged(nameof(CanResumeOrDiscardPlanning)); OnPropertyChanged(nameof(CanRemoveFromQueue)); } partial void OnHasQueuedSubtasksChanged(bool value) => OnPropertyChanged(nameof(CanRemoveFromQueue)); partial void OnBlockedByTaskIdChanged(string? value) { OnPropertyChanged(nameof(IsQueued)); OnPropertyChanged(nameof(IsWaiting)); OnPropertyChanged(nameof(StatusChipClass)); } partial void OnParentTaskIdChanged(string? value) { OnPropertyChanged(nameof(IsChild)); OnPropertyChanged(nameof(CanOpenPlanningSession)); } partial void OnHasPlanningChildrenChanged(bool value) => OnPropertyChanged(nameof(IsPlanningParent)); partial void OnBranchChanged(string? value) => OnPropertyChanged(nameof(HasBranch)); partial void OnLiveTailChanged(string? value) => OnPropertyChanged(nameof(HasLiveTail)); partial void OnDoneChanged(bool value) => OnPropertyChanged(nameof(IsOverdue)); partial void OnScheduledForChanged(DateTime? value) { OnPropertyChanged(nameof(IsOverdue)); OnPropertyChanged(nameof(HasSchedule)); } partial void OnDiffAdditionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffAdditionsText)); } partial void OnDiffDeletionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffDeletionsText)); } public static TaskRowViewModel FromEntity(TaskEntity t) { var row = new TaskRowViewModel { Id = t.Id, CreatedAt = t.CreatedAt }; row.UpdateFromEntity(t); return row; } public void UpdateFromEntity(TaskEntity t) { var (add, del) = ParseDiffStat(t.Worktree?.DiffStat); Title = t.Title; ListName = t.List?.Name ?? ""; Done = t.Status == TaskStatus.Done; IsStarred = t.IsStarred; IsMyDay = t.IsMyDay; Status = t.Status; Branch = t.Worktree?.BranchName; DiffStat = t.Worktree?.DiffStat; ScheduledFor = t.ScheduledFor; DiffAdditions = add; DiffDeletions = del; ParentTaskId = t.ParentTaskId; BlockedByTaskId = t.BlockedByTaskId; } // Best-effort parse of diff stat strings like "+12 -3" or "12 additions, 3 deletions". private static (int add, int del) ParseDiffStat(string? s) { if (string.IsNullOrWhiteSpace(s)) return (0, 0); int add = 0, del = 0; var parts = s.Split(new[] { ' ', ',', '\t' }, StringSplitOptions.RemoveEmptyEntries); foreach (var p in parts) { if (p.Length > 1 && p[0] == '+' && int.TryParse(p.AsSpan(1), out var a)) add = a; else if (p.Length > 1 && (p[0] == '-' || p[0] == '\u2212') && int.TryParse(p.AsSpan(1), out var d)) del = d; } return (add, del); } }