diff --git a/docs/open.md b/docs/open.md index 7dbe83aa..6406e337 100644 --- a/docs/open.md +++ b/docs/open.md @@ -15,7 +15,6 @@ Checkliste lebt in `docs/verification-handoff.md`. - **Planning-aktiver Parent zeigt weiter „Idle":** Parent in `planning_phase=active` hat `Status=Idle` (korrekt im Modell), aber `TaskRowViewModel.StatusLabel` (Zeile 132) kennt nur `HasInteractiveSession`/`IsParked` als Overrides — der `PlanningBadge` ersetzt den Chip nicht sichtbar → liest sich wie ein normaler Idle-Task. Wunsch: klarer „Planning/Draft aktiv"-Zustand, der Idle überschreibt. (Kinder zeigen korrekt „Draft".) - **Blocked-by-Kette nicht sichtbar:** Nach Finalize ist die sequentielle Kette korrekt gesetzt (child[i] blocked-by child[i-1]), aber die UI stellt Reihenfolge/Abhängigkeit nicht dar — es gibt nur den „waiting"-Chip. Wunsch: Kette visualisieren (z.B. „wartet auf "). -- **Dequeue-„X" fehlt auf wartenden (blockierten) Kettengliedern:** `CanRemoveFromQueue = IsQueued || HasQueuedSubtasks` (TaskRowViewModel.cs:99), `IsQueued` verlangt leeres `blocked_by`. Ein gequeuetes, aber blockiertes Kind (`IsWaiting`) bekommt daher kein Remove-from-queue-X — nur Parent + erstes (entsperrtes) Kind. - **Modal-Bodies unten abgeschnitten — gefixt, Sichtprüfung offen** (Ursache 2026-09-02 verifiziert: `ScrollViewer.Padding` zählt in Avalonia 12 nicht zum Extent; Fix ist das Padding auf ein `Margin` des inneren Inhalts umzuziehen). Betraf `AboutModalView`, `MergeHelperSelectionModal`, `MergeModalView`, `RepoImportModalView`, `WorktreesOverviewModalView` — alle fünf umgestellt. `ListSettingsModalView`, `LogVisualizerView` und `WorkConsole` hatten zu diesem Zeitpunkt bereits kein ScrollViewer-Padding mehr. - **Usage-Monitor-Modal: Analyse-Tabellen ausbaufähig** (Sichtprüfung 2026-08-06; Gauges, Info-Bänder, Presets/Custom-Range und Refresh-Button sind in Ordnung). Models-Tab: die Spaltenköpfe „OTHER CACHE" und „SHARE" kollidieren zu `OTHER CACHISHARE`; `claude-haiku-4-5-20251001` läuft in die IN-Spalte; alle Zahlen linksbündig und ohne Tausendertrenner (`506830708`). Tasks-Tab: Task-Titel wird ohne Ellipse hart an der LIST-Spalte abgeschnitten, der LIST-Text ebenfalls; MODEL ist bei Runs von vor der `task_runs.model`-Spalte leer (besser „—"). Wunsch: Zahlen rechtsbündig + `#,##0` (oder k/M), Spaltenbreiten/Truncation fixen. - **Conflict-Resolver: mehrere Konfliktdateien schlecht erkennbar:** Beim 2-Datei-Konflikt schwer zu sehen, dass zwei Dateien betroffen sind (File-Switcher/Anzahl zu unauffällig). Prominentere Datei-Liste / „x von y Dateien". diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs index e5cb1da5..600bf719 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs @@ -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 diff --git a/src/ClaudeDo.Worker/State/TaskStateService.cs b/src/ClaudeDo.Worker/State/TaskStateService.cs index 8c22f0fa..fd2e621a 100644 --- a/src/ClaudeDo.Worker/State/TaskStateService.cs +++ b/src/ClaudeDo.Worker/State/TaskStateService.cs @@ -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); } diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/TaskRowContextMenuTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/TaskRowContextMenuTests.cs index 1df432fd..f48da78d 100644 --- a/tests/ClaudeDo.Ui.Tests/ViewModels/TaskRowContextMenuTests.cs +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/TaskRowContextMenuTests.cs @@ -41,6 +41,23 @@ public class TaskRowContextMenuTests Assert.NotNull(row.SendToQueueDisabledReason); } + [Fact] + public void CanRemoveFromQueue_True_When_Queued_And_Blocked() + { + var row = MakeRow(); + row.Status = TaskStatus.Queued; + row.BlockedByTaskId = "predecessor-id"; + Assert.True(row.CanRemoveFromQueue); + } + + [Fact] + public void CanRemoveFromQueue_False_When_Idle() + { + var row = MakeRow(); + row.Status = TaskStatus.Idle; + Assert.False(row.CanRemoveFromQueue); + } + [Fact] public void CancelDisabledReason_Null_While_Running() { diff --git a/tests/ClaudeDo.Worker.Tests/State/TaskStateServiceTests.cs b/tests/ClaudeDo.Worker.Tests/State/TaskStateServiceTests.cs index 79c7de79..2e7781ed 100644 --- a/tests/ClaudeDo.Worker.Tests/State/TaskStateServiceTests.cs +++ b/tests/ClaudeDo.Worker.Tests/State/TaskStateServiceTests.cs @@ -585,6 +585,23 @@ public sealed class TaskStateServiceTests : IDisposable Assert.Equal(TaskStatus.Running, await GetStatusAsync(id)); } + [Fact] + public async Task DequeueToIdleAsync_MidChain_UnblocksSuccessor() + { + // pred (queued, blocked) is removed from the queue in the middle of a chain; + // successor was blocked on pred and must not be left wedged forever. + var pred = await SeedTaskAsync(TaskStatus.Queued); + var id = await SeedTaskAsync(TaskStatus.Queued, blockedBy: pred); + var successor = await SeedTaskAsync(TaskStatus.Queued, blockedBy: id); + + var result = await _sut.DequeueToIdleAsync(id, default); + + Assert.True(result.Ok); + var successorTask = await GetTaskAsync(successor); + Assert.Equal(TaskStatus.Queued, successorTask.Status); + Assert.Null(successorTask.BlockedByTaskId); + } + // ─── StartPlanningAsync ─────────────────────────────────────────────── [Fact] diff --git a/tests/ClaudeDo.Worker.Tests/UiVm/TaskRowViewModelNotificationsTests.cs b/tests/ClaudeDo.Worker.Tests/UiVm/TaskRowViewModelNotificationsTests.cs index d8fe1112..94c0ec16 100644 --- a/tests/ClaudeDo.Worker.Tests/UiVm/TaskRowViewModelNotificationsTests.cs +++ b/tests/ClaudeDo.Worker.Tests/UiVm/TaskRowViewModelNotificationsTests.cs @@ -281,6 +281,7 @@ public class TaskRowViewModelNotificationsTests nameof(TaskRowViewModel.IsQueued), nameof(TaskRowViewModel.IsWaiting), nameof(TaskRowViewModel.StatusChipClass), + nameof(TaskRowViewModel.CanRemoveFromQueue), }, raised); }