feat(planning): gate subtask queueing behind plan finalization
Planning subtasks are now "Draft" until their parent plan is finalized, then "Planned" (queueable). Finalizing a plan no longer auto-queues the child chain; the user sends the plan to the queue explicitly. - TaskStateService rejects a child entering Queued/Running unless its parent is Finalized; this single invariant covers UI, queue, RunNow and MCP paths - WorkerHub.SetTaskStatus routes Queued through the gated EnqueueAsync - Finalize call sites pass queueAgentTasks: false - PlanningChainCoordinator.QueuePlanAsync guards the chain build on Finalized - TaskRowViewModel derives Draft/Planned from ParentFinalized; gates CanSendToQueue / CanQueuePlan; view shows a PLANNED badge Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
[ObservableProperty] private bool _hasPlanningChildren;
|
||||
[ObservableProperty] private bool _hasQueuedSubtasks;
|
||||
[ObservableProperty] private bool _showListChip = true;
|
||||
[ObservableProperty] private bool _parentFinalized;
|
||||
|
||||
public DateTime CreatedAt { get; init; }
|
||||
public string CreatedAtFormatted => CreatedAt == default ? "—" : $"Created {CreatedAt:MMM d}";
|
||||
@@ -39,7 +40,9 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
public bool IsChild => !string.IsNullOrEmpty(ParentTaskId);
|
||||
public bool IsPlanningParent => PlanningPhase != PlanningPhase.None
|
||||
|| HasPlanningChildren;
|
||||
public bool IsDraft => IsChild && Status == TaskStatus.Idle;
|
||||
// A subtask is Draft until its planning parent is finalized, then Planned (queueable).
|
||||
public bool IsDraft => IsChild && Status == TaskStatus.Idle && !ParentFinalized;
|
||||
public bool IsPlanned => IsChild && Status == TaskStatus.Idle && ParentFinalized;
|
||||
|
||||
public bool CanOpenPlanningSession => Status == TaskStatus.Idle
|
||||
&& PlanningPhase == PlanningPhase.None
|
||||
@@ -61,7 +64,12 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
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 CanSendToQueue => !IsRunning && !IsQueued && !HasQueuedSubtasks;
|
||||
public bool CanSendToQueue => !IsRunning && !IsQueued && !HasQueuedSubtasks
|
||||
&& (!IsChild || ParentFinalized);
|
||||
// Parent-level "send plan to queue" — only once the plan is finalized (children Planned).
|
||||
public bool CanQueuePlan => !IsChild && HasPlanningChildren
|
||||
&& PlanningPhase == PlanningPhase.Finalized
|
||||
&& !HasQueuedSubtasks;
|
||||
public bool HasSchedule => ScheduledFor.HasValue;
|
||||
public bool HasLiveTail => IsRunning && !string.IsNullOrEmpty(LiveTail);
|
||||
|
||||
@@ -87,6 +95,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(IsWaiting));
|
||||
OnPropertyChanged(nameof(HasLiveTail));
|
||||
OnPropertyChanged(nameof(IsDraft));
|
||||
OnPropertyChanged(nameof(IsPlanned));
|
||||
OnPropertyChanged(nameof(CanOpenPlanningSession));
|
||||
OnPropertyChanged(nameof(CanRemoveFromQueue));
|
||||
OnPropertyChanged(nameof(CanSendToQueue));
|
||||
@@ -96,21 +105,32 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
{
|
||||
OnPropertyChanged(nameof(IsChild));
|
||||
OnPropertyChanged(nameof(IsDraft));
|
||||
OnPropertyChanged(nameof(IsPlanned));
|
||||
OnPropertyChanged(nameof(CanSendToQueue));
|
||||
OnPropertyChanged(nameof(CanOpenPlanningSession));
|
||||
}
|
||||
|
||||
partial void OnParentFinalizedChanged(bool value)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsDraft));
|
||||
OnPropertyChanged(nameof(IsPlanned));
|
||||
OnPropertyChanged(nameof(CanSendToQueue));
|
||||
}
|
||||
|
||||
partial void OnPlanningPhaseChanged(PlanningPhase value)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsPlanningParent));
|
||||
OnPropertyChanged(nameof(PlanningBadge));
|
||||
OnPropertyChanged(nameof(CanOpenPlanningSession));
|
||||
OnPropertyChanged(nameof(CanResumeOrDiscardPlanning));
|
||||
OnPropertyChanged(nameof(CanQueuePlan));
|
||||
}
|
||||
|
||||
partial void OnHasQueuedSubtasksChanged(bool value)
|
||||
{
|
||||
OnPropertyChanged(nameof(CanRemoveFromQueue));
|
||||
OnPropertyChanged(nameof(CanSendToQueue));
|
||||
OnPropertyChanged(nameof(CanQueuePlan));
|
||||
}
|
||||
|
||||
partial void OnBlockedByTaskIdChanged(string? value)
|
||||
@@ -121,7 +141,10 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
}
|
||||
|
||||
partial void OnHasPlanningChildrenChanged(bool value)
|
||||
=> OnPropertyChanged(nameof(IsPlanningParent));
|
||||
{
|
||||
OnPropertyChanged(nameof(IsPlanningParent));
|
||||
OnPropertyChanged(nameof(CanQueuePlan));
|
||||
}
|
||||
|
||||
partial void OnBranchChanged(string? value) => OnPropertyChanged(nameof(HasBranch));
|
||||
partial void OnLiveTailChanged(string? value) => OnPropertyChanged(nameof(HasLiveTail));
|
||||
|
||||
Reference in New Issue
Block a user