feat(ui): Task-Zeile entdichten und Kontextmenü gruppieren (UX-Struktur A)
Gruppe 1 (Send to queue, Remove from queue, Cancel execution, Open quick session, Refine task) ist jetzt fix sichtbar und gated per IsEnabled + Grund statt komplett auszublenden. Planning/Schedule wandern in Untermenüs (Mark as existierte bereits); der Planning-Kopf zeigt einen Grund, wenn er sonst leer wäre. Refine-Button, ungesetzter Star und Dequeue-X erscheinen nur bei Hover-oder-Selektion (TaskRowViewModel.ShowRowActions); ein gesetzter Star und der Refining-Spinner bleiben immer sichtbar. Der Branch-Chip wandert aus der Zeile in eine Meta-Zeile in TaskHeaderBar. A3 headless verifiziert: Avalonia 12 zeigt ToolTips auf IsEnabled=false Controls nur mit ToolTip.ShowOnDisabled="True" (Default ist false) — dieses Attached Property existiert bereits und wird für die neuen Disabled-Reason- Tooltips genutzt.
This commit is contained in:
@@ -58,6 +58,11 @@ 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;
|
||||
|
||||
// Set from PointerEntered/PointerExited in TaskRowView.axaml.cs. Drives ShowRowActions
|
||||
// together with IsSelected, so hover-only affordances (refine button, unset star, dequeue-X)
|
||||
// stay reachable via keyboard/selection, not just mouse hover.
|
||||
[ObservableProperty] private bool _isHovered;
|
||||
|
||||
// Transient: set from HubBroadcaster's OperationProgress while the worker is still creating
|
||||
// this task's worktree (the silent gap between Queued and the first agent output). Cleared
|
||||
// by the next entity refresh — UpdateFromEntity always reflects a settled state, so there's
|
||||
@@ -68,6 +73,10 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
// suppress the ordinary hover highlight/transitions so they don't fight the hint.
|
||||
public bool IsDropTarget => DropHintAbove || DropHintBelow;
|
||||
|
||||
// Hover-or-selected gate for the row's optional action affordances (refine button, unset
|
||||
// star, dequeue-X) — a set star and the IsRefining spinner are exempt, they show always.
|
||||
public bool ShowRowActions => IsHovered || IsSelected;
|
||||
|
||||
public bool CanRefine => Status == TaskStatus.Idle && PlanningPhase == PlanningPhase.None
|
||||
&& !IsRefining && !IsManual;
|
||||
|
||||
@@ -142,6 +151,60 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
&& !HasQueuedSubtasks;
|
||||
// User-triggered finalize for a planning parent whose session was closed before finalizing.
|
||||
public bool CanFinalizePlanning => PlanningPhase == PlanningPhase.Active && !IsChild;
|
||||
|
||||
// Context-menu Group 1 reasons: null when the paired CanX is true, otherwise the specific
|
||||
// blocking condition — shown as a disabled MenuItem's tooltip (ToolTip.ShowOnDisabled="True",
|
||||
// verified headlessly to actually render on Avalonia 12 — see task A3). Each mirrors the
|
||||
// negation of its CanX so exactly one branch matches whenever CanX is false.
|
||||
public string? SendToQueueDisabledReason
|
||||
{
|
||||
get
|
||||
{
|
||||
if (CanSendToQueue) return null;
|
||||
if (IsRunning) return Loc.T("tasks.reasonAlreadyRunning");
|
||||
if (IsQueued) return Loc.T("tasks.reasonAlreadyQueued");
|
||||
if (IsWaitingForReview) return Loc.T("tasks.reasonWaitingForReview");
|
||||
if (HasQueuedSubtasks) return Loc.T("tasks.reasonSubtasksQueued");
|
||||
if (IsChild && !ParentFinalized) return Loc.T("tasks.reasonPlanNotFinalized");
|
||||
if (PlanningPhase == PlanningPhase.Active) return Loc.T("tasks.reasonPlanningActive");
|
||||
if (IsManual) return Loc.T("tasks.manualTip");
|
||||
if (HasInteractiveSession) return Loc.T("tasks.reasonInteractiveSession");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public string? CancelDisabledReason => IsRunning ? null : Loc.T("tasks.reasonNotRunning");
|
||||
|
||||
// "Open quick session" has no precondition today (see A1's file header) — kept as a
|
||||
// placeholder so the menu wiring is uniform; always null until a real gate exists.
|
||||
public string? QuickSessionDisabledReason => null;
|
||||
|
||||
public string? RefineDisabledReason
|
||||
{
|
||||
get
|
||||
{
|
||||
if (CanRefine) return null;
|
||||
if (IsManual) return Loc.T("tasks.manualTip");
|
||||
if (IsRefining) return Loc.T("tasks.reasonRefining");
|
||||
if (PlanningPhase != PlanningPhase.None) return Loc.T("tasks.reasonIsPlanningParent");
|
||||
return Loc.T("tasks.reasonNotIdle");
|
||||
}
|
||||
}
|
||||
|
||||
// Gates the "Planning" submenu header — null unless Open/Resume-or-Discard/Finalize would
|
||||
// all be hidden, i.e. the submenu would otherwise open empty.
|
||||
public string? PlanningDisabledReason
|
||||
{
|
||||
get
|
||||
{
|
||||
if (CanOpenPlanningSession || CanResumeOrDiscardPlanning || CanFinalizePlanning) return null;
|
||||
if (IsManual) return Loc.T("tasks.manualTip");
|
||||
if (IsChild) return Loc.T("tasks.reasonIsChildTask");
|
||||
if (PlanningPhase == PlanningPhase.Finalized) return Loc.T("tasks.reasonPlanAlreadyFinalized");
|
||||
return Loc.T("tasks.reasonNotIdle");
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasSchedule => ScheduledFor.HasValue;
|
||||
// "Add to My Day" — shown on any task not already in My Day; a Done task has no place in
|
||||
// today's focus list. The mirror of "Remove from My Day" (gated on IsMyDay).
|
||||
@@ -243,6 +306,10 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(CanRemoveFromQueue));
|
||||
OnPropertyChanged(nameof(CanSendToQueue));
|
||||
OnPropertyChanged(nameof(CanRefine));
|
||||
OnPropertyChanged(nameof(SendToQueueDisabledReason));
|
||||
OnPropertyChanged(nameof(CancelDisabledReason));
|
||||
OnPropertyChanged(nameof(RefineDisabledReason));
|
||||
OnPropertyChanged(nameof(PlanningDisabledReason));
|
||||
}
|
||||
|
||||
partial void OnParentTaskIdChanged(string? value)
|
||||
@@ -254,6 +321,8 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(IsPlanned));
|
||||
OnPropertyChanged(nameof(CanSendToQueue));
|
||||
OnPropertyChanged(nameof(CanOpenPlanningSession));
|
||||
OnPropertyChanged(nameof(SendToQueueDisabledReason));
|
||||
OnPropertyChanged(nameof(PlanningDisabledReason));
|
||||
}
|
||||
|
||||
partial void OnParentInViewChanged(bool value)
|
||||
@@ -270,6 +339,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(IsDraft));
|
||||
OnPropertyChanged(nameof(IsPlanned));
|
||||
OnPropertyChanged(nameof(CanSendToQueue));
|
||||
OnPropertyChanged(nameof(SendToQueueDisabledReason));
|
||||
}
|
||||
|
||||
partial void OnPlanningPhaseChanged(PlanningPhase value)
|
||||
@@ -285,9 +355,16 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(CanSendToQueue));
|
||||
OnPropertyChanged(nameof(CanFinalizePlanning));
|
||||
OnPropertyChanged(nameof(CanRefine));
|
||||
OnPropertyChanged(nameof(SendToQueueDisabledReason));
|
||||
OnPropertyChanged(nameof(RefineDisabledReason));
|
||||
OnPropertyChanged(nameof(PlanningDisabledReason));
|
||||
}
|
||||
|
||||
partial void OnIsRefiningChanged(bool value) => OnPropertyChanged(nameof(CanRefine));
|
||||
partial void OnIsRefiningChanged(bool value)
|
||||
{
|
||||
OnPropertyChanged(nameof(CanRefine));
|
||||
OnPropertyChanged(nameof(RefineDisabledReason));
|
||||
}
|
||||
|
||||
partial void OnIsManualChanged(bool value)
|
||||
{
|
||||
@@ -295,6 +372,9 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(CanRefine));
|
||||
OnPropertyChanged(nameof(CanSendToQueue));
|
||||
OnPropertyChanged(nameof(CanOpenPlanningSession));
|
||||
OnPropertyChanged(nameof(SendToQueueDisabledReason));
|
||||
OnPropertyChanged(nameof(RefineDisabledReason));
|
||||
OnPropertyChanged(nameof(PlanningDisabledReason));
|
||||
}
|
||||
|
||||
partial void OnHasInteractiveSessionChanged(bool value)
|
||||
@@ -305,6 +385,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(ShowStatusChip));
|
||||
OnPropertyChanged(nameof(StatusChipTooltip));
|
||||
OnPropertyChanged(nameof(CanSendToQueue));
|
||||
OnPropertyChanged(nameof(SendToQueueDisabledReason));
|
||||
}
|
||||
|
||||
partial void OnFailureReasonChanged(string? value)
|
||||
@@ -323,8 +404,12 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(CanRemoveFromQueue));
|
||||
OnPropertyChanged(nameof(CanSendToQueue));
|
||||
OnPropertyChanged(nameof(CanQueuePlan));
|
||||
OnPropertyChanged(nameof(SendToQueueDisabledReason));
|
||||
}
|
||||
|
||||
partial void OnIsHoveredChanged(bool value) => OnPropertyChanged(nameof(ShowRowActions));
|
||||
partial void OnIsSelectedChanged(bool value) => OnPropertyChanged(nameof(ShowRowActions));
|
||||
|
||||
partial void OnBlockedByTaskIdChanged(string? value)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsQueued));
|
||||
|
||||
Reference in New Issue
Block a user