feat(tasks): mark tasks and lists as manual

Reminders written down as todos had no home: every task looked like Claude work.
A manual task now shows a MANUAL badge and hides send-to-queue, refine and the
planning session; the queue picker, daily prep and the list handler all skip it,
with a TaskStateService guard so the MCP surface and hub cannot start one either.
Opening a hand-driven ConPTY session stays available on purpose.

A list can be marked manual in its settings, which makes tasks created there
(UI and MCP add_task) start out manual. Toggle per task from its context menu.
This commit is contained in:
Mika Kuns
2026-07-27 15:02:51 +02:00
parent fde9615b34
commit 3a648b7d77
19 changed files with 168 additions and 11 deletions
@@ -35,6 +35,9 @@ public sealed partial class TaskRowViewModel : ViewModelBase
[ObservableProperty] private bool _parentInView = true;
[ObservableProperty] private int _roadblockCount;
[ObservableProperty] private bool _isRefining;
// Manual = a reminder only the user can do. Every "hand this to Claude" affordance is hidden
// and automation skips it; opening a hand-driven ConPTY session stays allowed.
[ObservableProperty] private bool _isManual;
// Set by the shell from Mission Control's open ConPTY panes: this task has a live hand-driven
// session, which outranks the persisted status on the lifecycle chip (an interactive task is
// typically Idle+Active-worktree, i.e. would otherwise read "Parked").
@@ -42,7 +45,10 @@ public sealed partial class TaskRowViewModel : ViewModelBase
// Set by the custom drag while this row is being dragged — drives the "grabbed" row style.
[ObservableProperty] private bool _isDragging;
public bool CanRefine => Status == TaskStatus.Idle && PlanningPhase == PlanningPhase.None && !IsRefining;
public bool CanRefine => Status == TaskStatus.Idle && PlanningPhase == PlanningPhase.None
&& !IsRefining && !IsManual;
public string? ManualBadge => IsManual ? Loc.T("tasks.badgeManual") : null;
public DateTime CreatedAt { get; init; }
public string CreatedAtFormatted => CreatedAt == default ? "—" : Loc.T("vm.taskRow.createdPrefix", CreatedAt.ToString("MMM d"));
@@ -64,7 +70,8 @@ public sealed partial class TaskRowViewModel : ViewModelBase
public bool CanOpenPlanningSession => Status == TaskStatus.Idle
&& PlanningPhase == PlanningPhase.None
&& !IsChild;
&& !IsChild
&& !IsManual;
public bool CanResumeOrDiscardPlanning => PlanningPhase == PlanningPhase.Active;
// Pick up in a terminal only where a session + worktree reliably still exist: a task
@@ -100,7 +107,8 @@ public sealed partial class TaskRowViewModel : ViewModelBase
// it must be finalized first.
public bool CanSendToQueue => !IsRunning && !IsQueued && !IsWaitingForReview && !HasQueuedSubtasks
&& (!IsChild || ParentFinalized)
&& PlanningPhase != PlanningPhase.Active;
&& PlanningPhase != PlanningPhase.Active
&& !IsManual;
// Parent-level "send plan to queue" — only once the plan is finalized (children Planned).
// Drives the routing inside SendToQueue, not a separate menu entry.
public bool CanQueuePlan => !IsChild && HasPlanningChildren
@@ -225,6 +233,14 @@ public sealed partial class TaskRowViewModel : ViewModelBase
partial void OnIsRefiningChanged(bool value) => OnPropertyChanged(nameof(CanRefine));
partial void OnIsManualChanged(bool value)
{
OnPropertyChanged(nameof(ManualBadge));
OnPropertyChanged(nameof(CanRefine));
OnPropertyChanged(nameof(CanSendToQueue));
OnPropertyChanged(nameof(CanOpenPlanningSession));
}
partial void OnHasInteractiveSessionChanged(bool value)
{
OnPropertyChanged(nameof(IsParked));
@@ -298,6 +314,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
Done = t.Status == TaskStatus.Done;
IsStarred = t.IsStarred;
IsMyDay = t.IsMyDay;
IsManual = t.IsManual;
Status = t.Status;
PlanningPhase = t.PlanningPhase;
Branch = t.Worktree?.BranchName;