Skip to content

Commit 12cfd18

Browse files
mortenasloclaude
andcommitted
feat: auto-cluster siblings + inherit active group on new sessions
Two follow-ups so the worktree clustering and category filter behave the way the user expects: - Auto-cluster reorder: when a session is added/removed or a session's RepoRoot resolves, RecomputeWorktreeSiblings now also pulls every session sharing a RepoRoot next to its first-seen anchor. Siblings always cluster — no longer dependent on the user creating them via the worktree dialog (which alone uses afterSessionId). The reorder is stable: first-occurrence order is preserved between clusters and for solo sessions, so unrelated sessions never get shuffled past each other. - The Sessions.CollectionChanged subscription now filters Action=Move out of the recompute path so the in-place reorder doesn't recurse, and a user drag-to-reorder isn't immediately undone. - New sessions launched from the toolbar/sidebar "+" while a real group filter is active now inherit that group automatically. Group resolution priority: explicit dialog selection > parent session's group > active filter group (skipping All/Ungrouped) > ungrouped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8509c29 commit 12cfd18

1 file changed

Lines changed: 85 additions & 9 deletions

File tree

src/CodeShellManager/MainWindow.xaml.cs

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,16 @@ public MainWindow()
105105
RebuildSidebarOrder();
106106
UpdateGroupTabIndicators();
107107
});
108-
_vm.Sessions.CollectionChanged += (_, _) =>
108+
_vm.Sessions.CollectionChanged += (_, e) =>
109109
{
110-
RecomputeWorktreeSiblings();
111-
UpdateGroupTabIndicators();
110+
// Skip on Move so the in-place reordering done by RecomputeWorktreeSiblings
111+
// (and user drag-to-reorder) doesn't recurse back into itself or fight the user.
112+
// Add/Remove/Reset are the cases that genuinely shift the sibling landscape.
113+
if (e.Action != System.Collections.Specialized.NotifyCollectionChangedAction.Move)
114+
{
115+
RecomputeWorktreeSiblings();
116+
UpdateGroupTabIndicators();
117+
}
112118
};
113119

114120
Loaded += OnLoaded;
@@ -315,10 +321,19 @@ private void OpenNewSessionDialogCore(string defaultFolder, SessionViewModel? pa
315321

316322
if (dialog.ShowDialog() != true) return;
317323

318-
// Inherit group from parent when present so siblings stay clustered.
324+
// Group resolution priority:
325+
// 1. Explicit selection from the dialog (currently unused — no group picker there)
326+
// 2. Inherited from a parent session (spawn-near-parent flows)
327+
// 3. The active group filter, when the user is currently looking at a real group
328+
// (not All / not Ungrouped) — new sessions land where the user expects them
329+
// 4. Ungrouped
319330
string? groupId = !string.IsNullOrEmpty(dialog.SelectedGroupId)
320331
? dialog.SelectedGroupId
321-
: (!string.IsNullOrEmpty(parent?.Session.GroupId) ? parent!.Session.GroupId : null);
332+
: !string.IsNullOrEmpty(parent?.Session.GroupId)
333+
? parent!.Session.GroupId
334+
: (_vm.ActiveGroupId != null && _vm.ActiveGroupId != GroupFilter.Ungrouped
335+
? _vm.ActiveGroupId
336+
: null);
322337

323338
var session = _sessionManager.CreateSession(
324339
dialog.SessionName,
@@ -1467,11 +1482,72 @@ private void RecomputeWorktreeSiblings()
14671482
anyChanged = true;
14681483
}
14691484
}
1470-
// When the cluster wrapper is enabled, sibling-state transitions need a sidebar
1471-
// rebuild so headers form/dissolve. The shared accent stripe + subtitle update
1472-
// independently via their own PropertyChanged subscriptions.
1473-
if (anyChanged && _vm.Settings.ShowWorktreeClusters)
1485+
1486+
// Auto-cluster: pull every session in a multi-sibling repo next to its anchor so
1487+
// siblings always group up, even when added/removed out of order or imported from
1488+
// a non-worktree creation path. This runs on Add/Remove/Reset and on RepoRoot
1489+
// resolve — but NOT on Move (the CollectionChanged filter skips it) so user
1490+
// drag-reorder is preserved.
1491+
bool reordered = ApplyClusteredOrder();
1492+
1493+
if ((anyChanged || reordered) && _vm.Settings.ShowWorktreeClusters)
14741494
RebuildSidebarOrder();
1495+
if (reordered)
1496+
_ = _vm.SaveStateAsync();
1497+
}
1498+
1499+
/// <summary>
1500+
/// Reorders <see cref="MainViewModel.Sessions"/> (and the underlying SessionManager
1501+
/// list) so every session sharing a RepoRoot sits adjacent to its first-seen anchor.
1502+
/// First-occurrence order is preserved between clusters and for solo sessions, so a
1503+
/// non-worktree session never gets shuffled past unrelated ones. Returns true when
1504+
/// at least one Move happened.
1505+
/// </summary>
1506+
private bool ApplyClusteredOrder()
1507+
{
1508+
if (_vm.Sessions.Count < 2) return false;
1509+
1510+
// Compute the desired order: stable group-by RepoRoot, anchored at first occurrence.
1511+
var desired = new List<SessionViewModel>(_vm.Sessions.Count);
1512+
var anchorIdx = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
1513+
foreach (var s in _vm.Sessions)
1514+
{
1515+
string key = !string.IsNullOrEmpty(s.RepoRoot) ? s.RepoRoot : "__solo:" + s.Id;
1516+
if (!anchorIdx.TryGetValue(key, out _))
1517+
{
1518+
anchorIdx[key] = desired.Count;
1519+
desired.Add(s);
1520+
}
1521+
else
1522+
{
1523+
// Insert immediately after the last existing member of this cluster.
1524+
int insertAt = anchorIdx[key];
1525+
for (int i = anchorIdx[key]; i < desired.Count; i++)
1526+
{
1527+
string k = !string.IsNullOrEmpty(desired[i].RepoRoot)
1528+
? desired[i].RepoRoot
1529+
: "__solo:" + desired[i].Id;
1530+
if (k == key) insertAt = i + 1;
1531+
}
1532+
desired.Insert(insertAt, s);
1533+
}
1534+
}
1535+
1536+
// Apply minimal Move operations to align current order with desired.
1537+
bool moved = false;
1538+
for (int i = 0; i < desired.Count; i++)
1539+
{
1540+
if (_vm.Sessions[i].Id == desired[i].Id) continue;
1541+
int j = -1;
1542+
for (int k = i + 1; k < _vm.Sessions.Count; k++)
1543+
if (_vm.Sessions[k].Id == desired[i].Id) { j = k; break; }
1544+
if (j <= i) continue;
1545+
// Mirror in the SessionManager model so state.json persists the new order.
1546+
_sessionManager.MoveSession(_vm.Sessions[j].Id, i);
1547+
_vm.Sessions.Move(j, i);
1548+
moved = true;
1549+
}
1550+
return moved;
14751551
}
14761552

14771553
// ── Per-session context menu ──────────────────────────────────────────────

0 commit comments

Comments
 (0)