fix(ui): stop the merge-helper tick from re-ticking unticked candidates

This commit is contained in:
mika kuns
2026-08-11 08:32:52 +02:00
parent 7106bf754f
commit d3ba279b54
2 changed files with 94 additions and 7 deletions
@@ -41,6 +41,10 @@ public sealed partial class MergeHelperSelectionModalViewModel : ViewModelBase
// Parent id -> its child rows, built after load; drives the tick-parent-ticks-children cascade.
private Dictionary<string, List<MergeHelperTaskRowViewModel>> _childrenByParentId = new();
// Set while ReconcileTickAsync restores remembered ticks, so replaying a parent's state
// doesn't cascade over its children's own remembered state.
private bool _suppressCascade;
public ObservableCollection<MergeHelperTaskRowViewModel> Tasks { get; } = new();
[ObservableProperty] private string _scopeLabel = "";
@@ -70,16 +74,31 @@ public sealed partial class MergeHelperSelectionModalViewModel : ViewModelBase
ScopeLabel = Loc.T("modals.mergeHelper.scopeList", listName);
}
// LoadAsync rebuilds every row from scratch, which would wipe the user's ticks out from
// under them on every tick — capture and restore selection by id around the reload.
// LoadAsync rebuilds every row from scratch and re-applies IsTickedByDefault, which would wipe
// the user's ticks out from under them on every tick — in BOTH directions. Remembering only
// the ticked ids would silently re-tick a row the user deliberately unticked (Idle/Queued/
// WaitingForReview/Failed all default to ticked), so carry the full state of every row that
// already existed across the reload; only genuinely new rows get the default.
internal async Task ReconcileTickAsync()
{
if (string.IsNullOrEmpty(_listId)) return;
var selected = Tasks.Where(t => t.IsSelected).Select(t => t.Id).ToHashSet();
var previous = Tasks.ToDictionary(t => t.Id, t => t.IsSelected);
await LoadAsync();
foreach (var t in Tasks)
if (selected.Contains(t.Id))
t.IsSelected = true;
// Restore without the parent→children cascade: it would overwrite a child's own remembered
// state with its parent's, undoing exactly the per-child choice being restored.
_suppressCascade = true;
try
{
foreach (var t in Tasks)
if (previous.TryGetValue(t.Id, out var wasSelected))
t.IsSelected = wasSelected;
}
finally
{
_suppressCascade = false;
}
OnPropertyChanged(nameof(CanConfirm));
}
@@ -162,7 +181,8 @@ public sealed partial class MergeHelperSelectionModalViewModel : ViewModelBase
// Ticking/unticking a parent cascades to its children; a child can still be
// deselected on its own afterwards without affecting the parent's checkbox —
// ClaudeDo has no indeterminate/tri-state parent checkbox.
if (sender is MergeHelperTaskRowViewModel row
if (!_suppressCascade
&& sender is MergeHelperTaskRowViewModel row
&& _childrenByParentId.TryGetValue(row.Id, out var children))
{
foreach (var child in children) child.IsSelected = row.IsSelected;