diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs index 87293a8..0824402 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs @@ -41,6 +41,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase // The task ID we are currently subscribed to for live log messages private string? _subscribedTaskId; + private CancellationTokenSource? _loadCts; + // Set by the view so OpenDiffCommand can show the modal as a dialog public Func? ShowDiffModal { get; set; } @@ -72,8 +74,13 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase Log.Add(new LogLineViewModel { Kind = kind, Text = line }); } - public async void Bind(TaskRowViewModel? row) + public void Bind(TaskRowViewModel? row) { + _loadCts?.Cancel(); + _loadCts?.Dispose(); + _loadCts = new CancellationTokenSource(); + var ct = _loadCts.Token; + Task = row; Log.Clear(); Subtasks.Clear(); @@ -90,26 +97,37 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase return; } - // Wire live-log subscription to new task - _subscribedTaskId = row.Id; + _ = BindAsync(row, ct); + } - await using var ctx = _dbFactory.CreateDbContext(); - var taskRepo = new TaskRepository(ctx); - var subtaskRepo = new SubtaskRepository(ctx); + private async System.Threading.Tasks.Task BindAsync(TaskRowViewModel row, CancellationToken ct) + { + try + { + await using var ctx = _dbFactory.CreateDbContext(); + var taskRepo = new TaskRepository(ctx); + var subtaskRepo = new SubtaskRepository(ctx); - var entity = await taskRepo.GetByIdAsync(row.Id); - if (entity == null) return; + var entity = await taskRepo.GetByIdAsync(row.Id); + ct.ThrowIfCancellationRequested(); + if (entity == null) return; - EditableTitle = entity.Title; - Notes = entity.Notes ?? ""; - Model = entity.Model; - WorktreePath = entity.Worktree?.Path; - BranchLine = entity.Worktree is { } w ? $"{w.BranchName} \u2190 main" : null; - AgentStatusLabel = entity.Status.ToString(); + EditableTitle = entity.Title; + Notes = entity.Notes ?? ""; + Model = entity.Model; + WorktreePath = entity.Worktree?.Path; + BranchLine = entity.Worktree is { } w ? $"{w.BranchName} \u2190 main" : null; + AgentStatusLabel = entity.Status.ToString(); - var subs = await subtaskRepo.GetByTaskIdAsync(row.Id); - foreach (var s in subs) - Subtasks.Add(new SubtaskRowViewModel { Id = s.Id, Title = s.Title, Done = s.Completed }); + // Subscribe only after DB load confirms the task exists + _subscribedTaskId = row.Id; + + var subs = await subtaskRepo.GetByTaskIdAsync(row.Id); + ct.ThrowIfCancellationRequested(); + foreach (var s in subs) + Subtasks.Add(new SubtaskRowViewModel { Id = s.Id, Title = s.Title, Done = s.Completed }); + } + catch (OperationCanceledException) { } } [RelayCommand(CanExecute = nameof(CanOpenDiff))] diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs index cea064d..b750701 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs @@ -12,6 +12,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase { private readonly IDbContextFactory _dbFactory; private ListNavItemViewModel? _currentList; + private CancellationTokenSource? _loadCts; public event EventHandler? SelectionChanged; public event EventHandler? FocusAddTaskRequested; @@ -30,8 +31,13 @@ public sealed partial class TasksIslandViewModel : ViewModelBase _dbFactory = dbFactory; } - public async void LoadForList(ListNavItemViewModel? list) + public void LoadForList(ListNavItemViewModel? list) { + _loadCts?.Cancel(); + _loadCts?.Dispose(); + _loadCts = new CancellationTokenSource(); + var ct = _loadCts.Token; + _currentList = list; Items.Clear(); if (list is null) return; @@ -39,27 +45,38 @@ public sealed partial class TasksIslandViewModel : ViewModelBase HeaderTitle = list.Name; HeaderEyebrow = DateTime.Now.ToString("dddd ยท MMM dd").ToUpperInvariant(); - await using var db = await _dbFactory.CreateDbContextAsync(); - var all = await db.Tasks - .Include(t => t.List) - .Include(t => t.Worktree) - .ToListAsync(); + _ = LoadForListAsync(list, ct); + } - IEnumerable filtered = list.Kind switch + private async Task LoadForListAsync(ListNavItemViewModel list, CancellationToken ct) + { + try { - ListKind.Smart when list.Id == "smart:my-day" => all.Where(t => t.IsMyDay), - ListKind.Smart when list.Id == "smart:important" => all.Where(t => t.IsStarred), - ListKind.Smart when list.Id == "smart:planned" => all.Where(t => t.ScheduledFor != null), - ListKind.Virtual when list.Id == "virtual:running" => all.Where(t => t.Status == TaskStatus.Running), - ListKind.Virtual when list.Id == "virtual:review" => all.Where(t => t.Status == TaskStatus.Done && t.Worktree?.State == WorktreeState.Active), - ListKind.User => all.Where(t => $"user:{t.ListId}" == list.Id), - _ => Enumerable.Empty(), - }; + await using var db = await _dbFactory.CreateDbContextAsync(ct); + var all = await db.Tasks + .Include(t => t.List) + .Include(t => t.Worktree) + .ToListAsync(ct); - foreach (var t in filtered) - Items.Add(TaskRowViewModel.FromEntity(t)); + ct.ThrowIfCancellationRequested(); - UpdateSubtitle(); + IEnumerable filtered = list.Kind switch + { + ListKind.Smart when list.Id == "smart:my-day" => all.Where(t => t.IsMyDay), + ListKind.Smart when list.Id == "smart:important" => all.Where(t => t.IsStarred), + ListKind.Smart when list.Id == "smart:planned" => all.Where(t => t.ScheduledFor != null), + ListKind.Virtual when list.Id == "virtual:running" => all.Where(t => t.Status == TaskStatus.Running), + ListKind.Virtual when list.Id == "virtual:review" => all.Where(t => t.Status == TaskStatus.Done && t.Worktree?.State == WorktreeState.Active), + ListKind.User => all.Where(t => $"user:{t.ListId}" == list.Id), + _ => Enumerable.Empty(), + }; + + foreach (var t in filtered) + Items.Add(TaskRowViewModel.FromEntity(t)); + + UpdateSubtitle(); + } + catch (OperationCanceledException) { } } private void UpdateSubtitle()