feat(ui): surface depends_on blocking in status chip, chain label, and detail pane
Usability-Audit 2026-09-10 found three gaps versus the dependency-chain-display spec: a Queued task blocked on an unmet task_dependencies edge rendered the same chip as one about to run, a cross-list predecessor silently dropped the "after #N" chip, and the detail pane showed nothing about a wait at all. - TaskRowViewModel gains IsBlockedByDependency/DependencyWaitLabel (extrinsic, assigned by TasksIslandViewModel like the existing chain fields); IsQueued/ IsWaiting/StatusChipClass/StatusLabel/StatusChipTooltip now account for it alongside the existing BlockedByTaskId planning-chain link. - TasksIslandViewModel.ApplyDependencyWaitState computes the blocked flag and wait label per row; a new RefreshExternalDependencyCacheAsync backfills Number/Title/Status for predecessors not loaded into the current list's Items (reusing the already-open DbContext, no new hub method), so BuildAfterLabel/blocking both resolve a listenfremder Vorgänger correctly. - New "waiting" chip style (IslandStyles.axaml) and locale keys (en+de); TaskHeaderBar shows a "Waiting on #N" line bound straight to the same row. Tests: TasksIslandChainTests (blocked/unblocked chip, cross-list predecessor), DetailsIslandDependencyWaitTests (new), TaskRowViewModelNotificationsTests updated for the new notification set.
This commit is contained in:
@@ -112,11 +112,31 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
[NotifyPropertyChangedFor(nameof(IsQueued))]
|
||||
[NotifyPropertyChangedFor(nameof(IsWaiting))]
|
||||
[NotifyPropertyChangedFor(nameof(StatusChipClass))]
|
||||
[NotifyPropertyChangedFor(nameof(StatusLabel))]
|
||||
[NotifyPropertyChangedFor(nameof(StatusChipTooltip))]
|
||||
[NotifyPropertyChangedFor(nameof(CanRemoveFromQueue))]
|
||||
private string? _blockedByTaskId;
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(DependsOnTaskId))]
|
||||
private IReadOnlyList<string> _dependsOnTaskIds = [];
|
||||
// Extrinsic — assigned by TasksIslandViewModel.ApplyDependencyWaitState, which alone has the
|
||||
// cross-row graph (plus any cross-list predecessor lookup) needed to tell an unmet
|
||||
// task_dependencies edge from a satisfied one. True only for the task_dependencies case;
|
||||
// IsWaiting also ORs in the older BlockedByTaskId planning-chain link.
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsQueued))]
|
||||
[NotifyPropertyChangedFor(nameof(IsWaiting))]
|
||||
[NotifyPropertyChangedFor(nameof(StatusChipClass))]
|
||||
[NotifyPropertyChangedFor(nameof(StatusLabel))]
|
||||
[NotifyPropertyChangedFor(nameof(StatusChipTooltip))]
|
||||
[NotifyPropertyChangedFor(nameof(CanRemoveFromQueue))]
|
||||
private bool _isBlockedByDependency;
|
||||
// The predecessor(s) this row is waiting on, rendered ("#42" or "#42, #57"), or null when not
|
||||
// waiting. Prefers BlockedByTaskId's single predecessor (the planning chain "parent wins" the
|
||||
// same way it does for the chain rail), else lists every unmet task_dependencies edge.
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(StatusChipTooltip))]
|
||||
private string? _dependencyWaitLabel;
|
||||
|
||||
// The single predecessor, or null when the task has none or several. The chain rail can only
|
||||
// draw a line, so a task waiting on n predecessors gets a ChainAfterLabel instead — see
|
||||
@@ -275,8 +295,8 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
// task). A live ConPTY session is that same shape but not parked — it reads "Interactive".
|
||||
public bool IsParked => Status == TaskStatus.Idle && WorktreeState == ClaudeDo.Data.Models.WorktreeState.Active
|
||||
&& !HasInteractiveSession;
|
||||
public bool IsQueued => Status == TaskStatus.Queued && string.IsNullOrEmpty(BlockedByTaskId);
|
||||
public bool IsWaiting => Status == TaskStatus.Queued && !string.IsNullOrEmpty(BlockedByTaskId);
|
||||
public bool IsQueued => Status == TaskStatus.Queued && string.IsNullOrEmpty(BlockedByTaskId) && !IsBlockedByDependency;
|
||||
public bool IsWaiting => Status == TaskStatus.Queued && (!string.IsNullOrEmpty(BlockedByTaskId) || IsBlockedByDependency);
|
||||
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 —
|
||||
@@ -412,9 +432,12 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
: FailureReasonLabel;
|
||||
|
||||
// Drives the status chip tooltip: an open interactive session takes priority (it's tappable),
|
||||
// otherwise a Failed task's reason, otherwise no tooltip.
|
||||
// then the predecessor(s) a waiting task is blocked on, then a Failed task's reason,
|
||||
// otherwise no tooltip.
|
||||
public string? StatusChipTooltip
|
||||
=> HasInteractiveSession ? Loc.T("tasks.interactiveChipTip") : FailureReasonTooltip;
|
||||
=> HasInteractiveSession ? Loc.T("tasks.interactiveChipTip")
|
||||
: IsWaiting && DependencyWaitLabel is not null ? Loc.T("tasks.waitingOnTip", DependencyWaitLabel)
|
||||
: FailureReasonTooltip;
|
||||
|
||||
public string DiffAdditionsText => $"+{DiffAdditions}";
|
||||
public string DiffDeletionsText => $"−{DiffDeletions}";
|
||||
@@ -422,7 +445,8 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
|
||||
public string StatusLabel
|
||||
=> HasInteractiveSession ? Loc.T("vm.taskStatus.interactive")
|
||||
: IsParked ? Loc.T("vm.taskStatus.parked") : Status switch
|
||||
: IsParked ? Loc.T("vm.taskStatus.parked")
|
||||
: IsWaiting ? Loc.T("vm.taskStatus.waiting") : Status switch
|
||||
{
|
||||
TaskStatus.Idle => Loc.T("vm.taskStatus.idle"),
|
||||
TaskStatus.Queued => Loc.T("vm.taskStatus.queued"),
|
||||
@@ -444,7 +468,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
|
||||
public string StatusChipClass => HasInteractiveSession
|
||||
? "interactive"
|
||||
: (Status, IsBlocked: !string.IsNullOrEmpty(BlockedByTaskId)) switch
|
||||
: (Status, IsBlocked: !string.IsNullOrEmpty(BlockedByTaskId) || IsBlockedByDependency) switch
|
||||
{
|
||||
(TaskStatus.Running, _) => "running",
|
||||
(TaskStatus.WaitingForReview, _) => "review",
|
||||
|
||||
Reference in New Issue
Block a user