feat(ui): add queueing and scheduling from task row context menu
- Right-click on a task row exposes Send to queue / Remove from queue and Schedule for... / Clear schedule actions. - New virtual:queued list in the sidebar with live count. - Sidebar counts are now computed (open per list, running, queued, review) and refreshed on task- and worker-side events. - Sending a task to the queue wakes the worker so it starts immediately.
This commit is contained in:
@@ -4,6 +4,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
@@ -12,11 +13,13 @@ namespace ClaudeDo.Ui.ViewModels.Islands;
|
||||
public sealed partial class TasksIslandViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
private readonly WorkerClient? _worker;
|
||||
private ListNavItemViewModel? _currentList;
|
||||
private CancellationTokenSource? _loadCts;
|
||||
|
||||
public event EventHandler? SelectionChanged;
|
||||
public event EventHandler? FocusAddTaskRequested;
|
||||
public event EventHandler? TasksChanged;
|
||||
public void RequestFocusAddTask() => FocusAddTaskRequested?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
public ObservableCollection<TaskRowViewModel> Items { get; } = new();
|
||||
@@ -38,9 +41,10 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
||||
[ObservableProperty] private bool _showOpenLabel;
|
||||
[ObservableProperty] private string _completedHeader = "COMPLETED";
|
||||
|
||||
public TasksIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory)
|
||||
public TasksIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, WorkerClient? worker = null)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_worker = worker;
|
||||
}
|
||||
|
||||
public void LoadForList(ListNavItemViewModel? list)
|
||||
@@ -85,6 +89,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
||||
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:queued" => all.Where(t => t.Status == TaskStatus.Queued),
|
||||
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),
|
||||
@@ -170,6 +175,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
||||
Regroup();
|
||||
NewTaskTitle = "";
|
||||
UpdateSubtitle();
|
||||
TasksChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public bool CanReorder => _currentList?.Kind == ListKind.User;
|
||||
@@ -199,15 +205,16 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
||||
if (source.IsRunning || target.IsRunning) return;
|
||||
if (ReferenceEquals(source, target)) return;
|
||||
|
||||
var srcIdx = Items.IndexOf(source);
|
||||
var tgtIdx = Items.IndexOf(target);
|
||||
if (srcIdx < 0 || tgtIdx < 0) return;
|
||||
// Master Items: single Move event (no Reset) so ItemsControls animate, not rebuild.
|
||||
MoveWithinCollection(Items, source, target, placeBelow);
|
||||
|
||||
Items.RemoveAt(srcIdx);
|
||||
var newTgtIdx = Items.IndexOf(target);
|
||||
var insertIdx = placeBelow ? newTgtIdx + 1 : newTgtIdx;
|
||||
if (insertIdx < 0 || insertIdx > Items.Count) insertIdx = Items.Count;
|
||||
Items.Insert(insertIdx, source);
|
||||
// Apply the same move in whichever section the row lives in.
|
||||
// Reorder never changes which section (Open/Overdue/Completed) a row belongs to —
|
||||
// that's determined by Done flag and ScheduledFor date, not drag-drop.
|
||||
var sourceSection = SectionFor(source);
|
||||
var targetSection = SectionFor(target);
|
||||
if (sourceSection is not null && ReferenceEquals(sourceSection, targetSection))
|
||||
MoveWithinCollection(sourceSection, source, target, placeBelow);
|
||||
|
||||
var listId = _currentList.Id["user:".Length..];
|
||||
var orderedIds = Items.Select(i => i.Id).ToList();
|
||||
@@ -223,8 +230,33 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
||||
if (e is not null) e.SortOrder = i;
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
Regroup();
|
||||
private static void MoveWithinCollection(
|
||||
System.Collections.ObjectModel.ObservableCollection<TaskRowViewModel> coll,
|
||||
TaskRowViewModel source,
|
||||
TaskRowViewModel target,
|
||||
bool placeBelow)
|
||||
{
|
||||
var srcIdx = coll.IndexOf(source);
|
||||
var tgtIdx = coll.IndexOf(target);
|
||||
if (srcIdx < 0 || tgtIdx < 0 || srcIdx == tgtIdx) return;
|
||||
|
||||
var finalIdx = placeBelow ? tgtIdx + 1 : tgtIdx;
|
||||
if (srcIdx < finalIdx) finalIdx--;
|
||||
if (finalIdx < 0) finalIdx = 0;
|
||||
if (finalIdx >= coll.Count) finalIdx = coll.Count - 1;
|
||||
if (finalIdx == srcIdx) return;
|
||||
|
||||
coll.Move(srcIdx, finalIdx);
|
||||
}
|
||||
|
||||
private System.Collections.ObjectModel.ObservableCollection<TaskRowViewModel>? SectionFor(TaskRowViewModel row)
|
||||
{
|
||||
if (OverdueItems.Contains(row)) return OverdueItems;
|
||||
if (OpenItems.Contains(row)) return OpenItems;
|
||||
if (CompletedItems.Contains(row)) return CompletedItems;
|
||||
return null;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -241,6 +273,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
||||
}
|
||||
Regroup();
|
||||
UpdateSubtitle();
|
||||
TasksChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -254,8 +287,61 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
||||
entity.IsStarred = row.IsStarred;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
TasksChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SendToQueueAsync(TaskRowViewModel? row)
|
||||
{
|
||||
if (row is null || row.IsRunning) return;
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id);
|
||||
if (entity is null) return;
|
||||
entity.Status = TaskStatus.Queued;
|
||||
await db.SaveChangesAsync();
|
||||
row.Status = TaskStatus.Queued;
|
||||
if (_worker is not null)
|
||||
{
|
||||
try { await _worker.WakeQueueAsync(); } catch { }
|
||||
}
|
||||
Regroup();
|
||||
UpdateSubtitle();
|
||||
TasksChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RemoveFromQueueAsync(TaskRowViewModel? row)
|
||||
{
|
||||
if (row is null) return;
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id);
|
||||
if (entity is null) return;
|
||||
entity.Status = TaskStatus.Manual;
|
||||
await db.SaveChangesAsync();
|
||||
row.Status = TaskStatus.Manual;
|
||||
Regroup();
|
||||
UpdateSubtitle();
|
||||
TasksChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public async Task SetScheduledForAsync(TaskRowViewModel row, DateTime? when)
|
||||
{
|
||||
if (row is null) return;
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id);
|
||||
if (entity is null) return;
|
||||
entity.ScheduledFor = when;
|
||||
await db.SaveChangesAsync();
|
||||
row.ScheduledFor = when;
|
||||
Regroup();
|
||||
UpdateSubtitle();
|
||||
TasksChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private Task ClearScheduleAsync(TaskRowViewModel? row) =>
|
||||
row is null ? Task.CompletedTask : SetScheduledForAsync(row, null);
|
||||
|
||||
[RelayCommand]
|
||||
private void Select(TaskRowViewModel row) => SelectedTask = row;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user