diff --git a/src/ClaudeDo.Ui/Design/IslandStyles.axaml b/src/ClaudeDo.Ui/Design/IslandStyles.axaml
index 84eeeb4..dc8645f 100644
--- a/src/ClaudeDo.Ui/Design/IslandStyles.axaml
+++ b/src/ClaudeDo.Ui/Design/IslandStyles.axaml
@@ -88,6 +88,10 @@
M13 4 H20 V11 H18 V7.4 L11.4 14 L10 12.6 L16.6 6 H13 Z M4 6 H10 V8 H6 V18 H16 V14 H18 V20 H4 Z
+
+ M9 5 L16 12 L9 19 L6.8 16.8 L11.6 12 L6.8 7.2 Z
+ M5 9 L12 16 L19 9 L16.8 6.8 L12 11.6 L7.2 6.8 Z
+
M4 6 H20 V8 H4 Z M4 11 H20 V13 H4 Z M4 16 H14 V18 H4 Z
diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs
index e7c7fde..fa40b2d 100644
--- a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs
@@ -301,27 +301,17 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
internal void Regroup()
{
- OverdueItems.Clear();
- OpenItems.Clear();
- CompletedItems.Clear();
-
- // Auto-collapse planning parents whose every child is Done (unless the user
- // has explicitly toggled the row — saved state wins).
+ // Collapse parents that have children by default, so subtasks stay tucked away until
+ // the user expands the row (an explicit toggle is saved and wins over this default).
var childrenByParent = Items
.Where(r => r.IsChild && !string.IsNullOrEmpty(r.ParentTaskId))
.GroupBy(r => r.ParentTaskId!)
.ToDictionary(g => g.Key, g => g.ToList());
- foreach (var parent in Items.Where(r => r.IsPlanningParent && !r.IsChild
- && r.PlanningPhase == PlanningPhase.Finalized
- && !r.Done))
+ foreach (var parent in Items.Where(r => r.IsPlanningParent && !r.IsChild && !r.Done))
{
if (_expandedState.ContainsKey(parent.Id)) continue;
- if (childrenByParent.TryGetValue(parent.Id, out var kids)
- && kids.Count > 0
- && kids.All(c => c.Status == TaskStatus.Done))
- {
+ if (childrenByParent.TryGetValue(parent.Id, out var kids) && kids.Count > 0)
parent.IsExpanded = false;
- }
}
// Restore IsExpanded from saved state
@@ -362,19 +352,29 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
}
var today = DateTime.Today;
+ var overdue = new List();
+ var open = new List();
+ var completed = new List();
foreach (var r in flat)
{
var underOpenPlanningParent = r.IsChild &&
flat.Any(p => p.Id == r.ParentTaskId && p.IsPlanningParent && !p.Done);
if (r.Done && !underOpenPlanningParent)
- CompletedItems.Add(r);
+ completed.Add(r);
else if (r.ScheduledFor is { } d && d.Date < today)
- OverdueItems.Add(r);
+ overdue.Add(r);
else
- OpenItems.Add(r);
+ open.Add(r);
}
+ // Reconcile the bound collections in place (granular Insert/Move/Remove) rather than
+ // Clear+Add, so toggling a parent only touches its own child rows — the ItemsControl
+ // keeps every unchanged container instead of tearing the whole list down on a Reset.
+ SyncCollection(OverdueItems, overdue);
+ SyncCollection(OpenItems, open);
+ SyncCollection(CompletedItems, completed);
+
HasOverdue = OverdueItems.Count > 0;
HasOpen = OpenItems.Count > 0;
HasCompleted = CompletedItems.Count > 0;
@@ -500,6 +500,28 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
coll.Move(srcIdx, finalIdx);
}
+ // Reconcile a bound collection toward a target order using granular Remove/Move/Insert,
+ // so unchanged rows keep their containers (no Reset-driven full re-render).
+ private static void SyncCollection(
+ System.Collections.ObjectModel.ObservableCollection dst,
+ List target)
+ {
+ var keep = new HashSet(target);
+ for (int i = dst.Count - 1; i >= 0; i--)
+ if (!keep.Contains(dst[i]))
+ dst.RemoveAt(i);
+
+ for (int i = 0; i < target.Count; i++)
+ {
+ var item = target[i];
+ if (i < dst.Count && ReferenceEquals(dst[i], item)) continue;
+
+ var cur = dst.IndexOf(item);
+ if (cur >= 0) dst.Move(cur, i);
+ else dst.Insert(i, item);
+ }
+ }
+
private System.Collections.ObjectModel.ObservableCollection? SectionFor(TaskRowViewModel row)
{
if (OverdueItems.Contains(row)) return OverdueItems;
diff --git a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml
index 6757347..c4a5cf6 100644
--- a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml
+++ b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml
@@ -87,10 +87,10 @@
VerticalAlignment="Center"
ToolTip.Tip="{loc:Tr tasks.toggleSubtasksTip}">
-
-
+
+
diff --git a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs
index b54da10..e99c74c 100644
--- a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs
+++ b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs
@@ -1,12 +1,7 @@
using System.Linq;
-using Avalonia;
-using Avalonia.Animation;
-using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
-using Avalonia.Media;
-using Avalonia.Styling;
using Avalonia.VisualTree;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.ViewModels.Islands;
@@ -123,24 +118,4 @@ public partial class TaskRowView : UserControl
ScheduleAnchor.Flyout?.Hide();
_pendingScheduleRow = null;
}
-
- protected override async void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
- {
- base.OnAttachedToVisualTree(e);
- RenderTransform = new TranslateTransform(0, 8);
- Opacity = 0;
- var anim = new Avalonia.Animation.Animation
- {
- Duration = TimeSpan.FromMilliseconds(300),
- Easing = new CubicEaseOut(),
- Children =
- {
- new KeyFrame { Cue = new Cue(0), Setters = { new Setter(OpacityProperty, 0d) } },
- new KeyFrame { Cue = new Cue(1), Setters = { new Setter(OpacityProperty, 1d) } },
- }
- };
- await anim.RunAsync(this);
- Opacity = 1;
- RenderTransform = null;
- }
}
diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/TasksIslandRegroupTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/TasksIslandRegroupTests.cs
index 7ced49e..3598dfc 100644
--- a/tests/ClaudeDo.Ui.Tests/ViewModels/TasksIslandRegroupTests.cs
+++ b/tests/ClaudeDo.Ui.Tests/ViewModels/TasksIslandRegroupTests.cs
@@ -168,6 +168,9 @@ public class TasksIslandRegroupTests : IDisposable
var vm = BuildViewModel();
await LoadAndWaitAsync(vm, UserList("list1", "Default"));
+ // Parents with children collapse by default; expand to surface the nested child.
+ vm.ToggleExpandCommand.Execute(vm.Items.First(r => r.Id == "p1"));
+
// Child with Done status under an open Planning parent should NOT go to CompletedItems
Assert.DoesNotContain(vm.CompletedItems, r => r.Id == "c1");
// Child should appear nested (IsChild == true) in OpenItems
diff --git a/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs b/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs
index 8bfff79..1aa3b11 100644
--- a/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs
+++ b/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs
@@ -165,7 +165,7 @@ public class TasksIslandViewModelPlanningTests
=> new TaskRowViewModel { Id = id, Status = status, ParentTaskId = parentId, PlanningPhase = phase };
[Fact]
- public void ToggleExpand_CollapsesChildrenOfPlanningParent()
+ public void PlanningParentWithChildren_CollapsedByDefault_ToggleExpands()
{
var parent = MakeRow("p1", TaskStatus.Idle, phase: PlanningPhase.Active);
var child1 = MakeRow("c1", TaskStatus.Idle, "p1");
@@ -173,18 +173,17 @@ public class TasksIslandViewModelPlanningTests
var (vm, _) = VmFactory.Create([parent, child1, child2]);
- // Initially expanded — children visible in OpenItems
- Assert.Contains(child1, vm.OpenItems);
- Assert.Contains(child2, vm.OpenItems);
-
- // Collapse the parent
- vm.ToggleExpandCommand.Execute(parent);
-
- // Children should no longer appear
+ // Collapsed by default — children hidden, parent still present
Assert.DoesNotContain(child1, vm.OpenItems);
Assert.DoesNotContain(child2, vm.OpenItems);
- // Parent still present
Assert.Contains(parent, vm.OpenItems);
+
+ // Expand the parent
+ vm.ToggleExpandCommand.Execute(parent);
+
+ // Children now visible
+ Assert.Contains(child1, vm.OpenItems);
+ Assert.Contains(child2, vm.OpenItems);
}
[Fact]
@@ -210,20 +209,23 @@ public class TasksIslandViewModelPlanningTests
}
[Fact]
- public void ToggleExpand_ExpandsCollapsedParentAgain()
+ public void ToggleExpand_TogglesParentExpansion()
{
var parent = MakeRow("p1", TaskStatus.Idle, phase: PlanningPhase.Finalized);
var child = MakeRow("c1", TaskStatus.Idle, "p1");
var (vm, _) = VmFactory.Create([parent, child]);
- // Collapse
- vm.ToggleExpandCommand.Execute(parent);
+ // Collapsed by default
Assert.DoesNotContain(child, vm.OpenItems);
- // Re-expand
+ // Expand
vm.ToggleExpandCommand.Execute(parent);
Assert.Contains(child, vm.OpenItems);
+
+ // Collapse again
+ vm.ToggleExpandCommand.Execute(parent);
+ Assert.DoesNotContain(child, vm.OpenItems);
}
[Fact]