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:
mika kuns
2026-04-23 13:07:48 +02:00
parent 9952ff98f2
commit 6f725d12f5
7 changed files with 280 additions and 21 deletions

View File

@@ -2,6 +2,8 @@ using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Modals;
@@ -68,7 +70,12 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
: Environment.UserName.ToUpperInvariant();
if (_worker is not null)
_worker.ListUpdatedEvent += id => _ = RefreshRowAsync(id);
{
_worker.ListUpdatedEvent += id => _ = RefreshRowAsync(id);
_worker.TaskStartedEvent += (_slot, _id, _at) => _ = RefreshCountsAsync();
_worker.TaskFinishedEvent += (_slot, _id, _status, _at) => _ = RefreshCountsAsync();
_worker.TaskUpdatedEvent += _id => _ = RefreshCountsAsync();
}
}
public async Task LoadAsync(CancellationToken ct = default)
@@ -82,6 +89,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
new ListNavItemViewModel { Id = "smart:my-day", Name = "My Day", Kind = ListKind.Smart, IconKey = "Sun" },
new ListNavItemViewModel { Id = "smart:important", Name = "Important", Kind = ListKind.Smart, IconKey = "Star" },
new ListNavItemViewModel { Id = "smart:planned", Name = "Planned", Kind = ListKind.Smart, IconKey = "Calendar" },
new ListNavItemViewModel { Id = "virtual:queued", Name = "Queue", Kind = ListKind.Virtual, IconKey = "Inbox" },
new ListNavItemViewModel { Id = "virtual:running", Name = "Running", Kind = ListKind.Virtual, IconKey = "Activity" },
new ListNavItemViewModel { Id = "virtual:review", Name = "Review", Kind = ListKind.Virtual, IconKey = "Eye" },
};
@@ -116,8 +124,46 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
public async Task RefreshCountsAsync(CancellationToken ct = default)
{
foreach (var i in Items) i.Count = 0;
await Task.CompletedTask;
try
{
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
// Snapshot the open (non-Done) tasks once; small enough collection for client-side grouping.
var open = await ctx.Tasks.AsNoTracking()
.Where(t => t.Status != TaskStatus.Done)
.Select(t => new { t.ListId, t.Status, t.IsMyDay, t.IsStarred, Scheduled = t.ScheduledFor })
.ToListAsync(ct);
var running = open.Count(t => t.Status == TaskStatus.Running);
var queued = open.Count(t => t.Status == TaskStatus.Queued);
var review = await ctx.Tasks.AsNoTracking()
.Where(t => t.Status == TaskStatus.Done && t.Worktree != null && t.Worktree.State == WorktreeState.Active)
.CountAsync(ct);
foreach (var item in SmartLists)
{
item.Count = item.Id switch
{
"smart:my-day" => open.Count(t => t.IsMyDay),
"smart:important" => open.Count(t => t.IsStarred),
"smart:planned" => open.Count(t => t.Scheduled != null),
"virtual:queued" => queued,
"virtual:running" => running,
"virtual:review" => review,
_ => 0,
};
}
foreach (var item in UserLists)
{
var listId = item.Id.StartsWith("user:", StringComparison.Ordinal)
? item.Id["user:".Length..]
: item.Id;
item.Count = open.Count(t => t.ListId == listId);
}
}
catch (OperationCanceledException) { throw; }
catch { /* best-effort refresh */ }
}
[RelayCommand]