diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs
index 9b7e576..6090e92 100644
--- a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs
@@ -26,6 +26,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
[ObservableProperty] private string? _parentTaskId;
[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}";
@@ -58,6 +59,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
public bool IsRunning => Status == TaskStatus.Running;
public bool IsQueued => Status == TaskStatus.Queued;
public bool IsWaiting => Status == TaskStatus.Waiting;
+ public bool CanRemoveFromQueue => IsQueued || HasQueuedSubtasks;
public bool HasSchedule => ScheduledFor.HasValue;
public bool HasLiveTail => IsRunning && !string.IsNullOrEmpty(LiveTail);
@@ -87,8 +89,12 @@ public sealed partial class TaskRowViewModel : ViewModelBase
OnPropertyChanged(nameof(IsDraft));
OnPropertyChanged(nameof(CanOpenPlanningSession));
OnPropertyChanged(nameof(CanResumeOrDiscardPlanning));
+ OnPropertyChanged(nameof(CanRemoveFromQueue));
}
+ partial void OnHasQueuedSubtasksChanged(bool value)
+ => OnPropertyChanged(nameof(CanRemoveFromQueue));
+
partial void OnParentTaskIdChanged(string? value)
{
OnPropertyChanged(nameof(IsChild));
diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs
index 4547958..bd26485 100644
--- a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs
@@ -100,6 +100,15 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
else return;
}
+ // Keep the parent's HasQueuedSubtasks flag in sync when a child's status flips.
+ if (entity is not null && !string.IsNullOrEmpty(entity.ParentTaskId))
+ {
+ var parent = Items.FirstOrDefault(r => r.Id == entity.ParentTaskId);
+ if (parent is not null)
+ parent.HasQueuedSubtasks = Items.Any(r =>
+ r.ParentTaskId == parent.Id && (r.IsQueued || r.IsWaiting));
+ }
+
Regroup();
UpdateSubtitle();
}
@@ -207,6 +216,16 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
if (parentsWithChildren.Contains(r.Id))
r.HasPlanningChildren = true;
+ // Mark planning parents whose children are currently queued/waiting,
+ // so the dequeue affordance is visible on the parent row.
+ var parentsWithQueuedKids = Items
+ .Where(r => r.IsChild && !string.IsNullOrEmpty(r.ParentTaskId)
+ && (r.IsQueued || r.IsWaiting))
+ .Select(r => r.ParentTaskId!)
+ .ToHashSet();
+ foreach (var r in Items)
+ r.HasQueuedSubtasks = parentsWithQueuedKids.Contains(r.Id);
+
Regroup();
UpdateSubtitle();
}
@@ -446,9 +465,15 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
{
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);
+ var entity = await db.Tasks.Include(t => t.Tags).FirstOrDefaultAsync(t => t.Id == row.Id);
if (entity is null) return;
entity.Status = TaskStatus.Queued;
+ // Worker queue picker requires the "agent" tag — attach it on explicit enqueue.
+ if (!entity.Tags.Any(t => t.Name == "agent"))
+ {
+ var agentTag = await db.Tags.FirstOrDefaultAsync(t => t.Name == "agent");
+ if (agentTag is not null) entity.Tags.Add(agentTag);
+ }
await db.SaveChangesAsync();
row.Status = TaskStatus.Queued;
if (_worker is not null)
@@ -467,9 +492,30 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
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;
+
+ // For a planning parent the dequeue button targets queued/waiting children,
+ // not the parent itself (whose Status is Planning/Planned).
+ if (entity.Status == TaskStatus.Planning || entity.Status == TaskStatus.Planned)
+ {
+ var children = await db.Tasks
+ .Where(t => t.ParentTaskId == row.Id
+ && (t.Status == TaskStatus.Queued || t.Status == TaskStatus.Waiting))
+ .ToListAsync();
+ foreach (var c in children) c.Status = TaskStatus.Manual;
+ await db.SaveChangesAsync();
+ foreach (var c in children)
+ {
+ var childRow = Items.FirstOrDefault(r => r.Id == c.Id);
+ if (childRow is not null) childRow.Status = TaskStatus.Manual;
+ }
+ row.HasQueuedSubtasks = false;
+ }
+ else
+ {
+ entity.Status = TaskStatus.Manual;
+ await db.SaveChangesAsync();
+ row.Status = TaskStatus.Manual;
+ }
Regroup();
UpdateSubtitle();
TasksChanged?.Invoke(this, EventArgs.Empty);
diff --git a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml
index 14cc299..73d2545 100644
--- a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml
+++ b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml
@@ -35,7 +35,7 @@
IsVisible="{Binding !IsQueued}"
Click="OnSendToQueueClick"/>
-
+