fix(ui): Dequeue-X auf blockierten Queue-Zeilen anzeigen und Kette konsistent halten

CanRemoveFromQueue schloss IsWaiting (queued+blocked) faelschlich aus, sodass ein
Kettenglied in der Mitte nicht entfernbar war. Zusaetzlich unblockt
TaskStateService.DequeueToIdleAsync jetzt den Nachfolger eines dequeueten
Kettenglieds, sonst bliebe er dauerhaft auf einen nicht mehr laufenden
Vorgaenger blockiert.
This commit is contained in:
mika kuns
2026-09-02 22:34:50 +02:00
parent 77c70d74cb
commit df3e7ba0c7
6 changed files with 48 additions and 2 deletions
@@ -112,6 +112,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
[NotifyPropertyChangedFor(nameof(IsQueued))]
[NotifyPropertyChangedFor(nameof(IsWaiting))]
[NotifyPropertyChangedFor(nameof(StatusChipClass))]
[NotifyPropertyChangedFor(nameof(CanRemoveFromQueue))]
private string? _blockedByTaskId;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(DependsOnTaskId))]
@@ -276,7 +277,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
&& !HasInteractiveSession;
public bool IsQueued => Status == TaskStatus.Queued && string.IsNullOrEmpty(BlockedByTaskId);
public bool IsWaiting => Status == TaskStatus.Queued && !string.IsNullOrEmpty(BlockedByTaskId);
public bool CanRemoveFromQueue => IsQueued || HasQueuedSubtasks;
public bool CanRemoveFromQueue => IsQueued || IsWaiting || HasQueuedSubtasks;
// "Send to queue" is the single queue entry. On a finalized planning parent it queues the
// plan (children) via CanQueuePlan; an Active (not-yet-finalized) planning parent is hidden —
// it must be finalized first. The worker never sees a UI-hosted ConPTY session (it never
@@ -430,6 +430,17 @@ public sealed class TaskStateService : ITaskStateService
if (affected == 0)
return new TransitionResult(false, "Task is not queued; cannot remove from queue.");
// A dequeued task may have been a planning-chain predecessor. Without unblocking its
// successor here, the successor is left permanently wedged on a BlockedByTaskId link to
// a task that has left the queue and will never reach the terminal state that would
// otherwise resolve it (PlanningChainCoordinator.OnChildFinishedAsync).
var successorId = await ctx.Tasks.AsNoTracking()
.Where(t => t.BlockedByTaskId == taskId)
.Select(t => t.Id)
.FirstOrDefaultAsync(ct);
if (successorId is not null)
await UnblockAsync(successorId, ct);
await NotifyAsync(taskId);
return new TransitionResult(true, null);
}