Skip to content

fix(layout): keep a restarted or woken pane in its sidebar position - #159

Closed
AThraen wants to merge 1 commit into
mainfrom
fix/restart-pane-order
Closed

AThraen wants to merge 1 commit into
mainfrom
fix/restart-pane-order

Conversation

@AThraen

@AThraen AThraen commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Problem

After ↻ Restart (and also after waking a 💤 session), the pane sometimes jumps to the end of the terminal grid while its sidebar row stays where it was.

Root cause

MainViewModel.RegisterSession decides where a freshly launched session's VM goes. It found the session's index in SessionManager.Sessions and inserted at that same index in MainViewModel.Sessions. Those are different index spaces:

  • SessionManager.Sessions holds live and dormant sessions, plus sessions the restore loop hasn't launched yet.
  • MainViewModel.Sessions holds only live sessions.

Restart and wake re-insert a VM whose sidebar row never moved. The pane therefore landed one slot too far down for every dormant or not-yet-live session above it. When that ran past the end of the list, it was appended last.

Example: sidebar A, 💤D, B, C. Restart B → its model index is 2 and the live list is [A, C] → Insert(2) → [A, C, B].

With no dormant sessions the two index spaces coincide, which is why it was intermittent.

Fix

Insert after the live sessions that precede it in the model order. This lives in the new WPF-free Services/SessionOrder.LiveInsertIndex, so it is unit-tested.

The same code path also fixes wake and insert-after-parent (duplicate/worktree) when a dormant session sits above.

Tests

  • New SessionOrderTests (7 cases: dormant above, first/last, several dormant, restore-in-progress, no dormant, unknown id).
  • 5 of the 7 fail against the previous logic. This was verified by running them against a copy of the old algorithm before the fix.
  • Full CodeShellManager.Tests: 619/619 passing (Release).

Not in scope

#131's broader divergence: group sectioning that only the sidebar applies, and drag-drop index spaces. This PR fixes only the re-insert case.

🤖 Generated with Claude Code

RegisterSession took the session's index in SessionManager.Sessions and
inserted at that index in MainViewModel.Sessions. Those are different
index spaces: the model also holds dormant sessions and ones the restore
loop hasn't launched yet. Restart and wake re-insert a VM whose sidebar
row never moved, so the pane drifted one slot down per such session above
it — often all the way to the end of the grid.

Insert after the live sessions that precede it in the model instead.
The rule lives in the WPF-free SessionOrder so it is unit-tested; five
of the seven new tests fail against the previous logic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mortenaslo added a commit that referenced this pull request Nov 6, 2026
PR #159 fixed the same RegisterSession index-space bug with its own
SessionOrder.LiveInsertIndex; this branch fixes it with InsertionIndex. Its
six placement cases (dormant above, first/last, several dormant, restore in
progress, nothing dormant) pass unchanged against InsertionIndex, so #159 can
close without losing its coverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX
@mortenaslo

Copy link
Copy Markdown
Contributor

Closing as superseded by #173. Both fix the same RegisterSession index-space bug — inserting at the SessionManager index into the live-only list — and they conflicted (each adds its own Services/SessionOrder.cs). #173 fixes it with SessionOrder.InsertionIndex (insert before the first live session ranked after it in the model order), which is the same placement, and also uses it for drag-reorder and grid alignment.

Your six placement cases (dormant above, first/last, several dormant, restore in progress, nothing dormant) are carried over unchanged as Restart_or_wake_lands_at_its_sidebar_position in 13a1512, and pass against InsertionIndex. Thanks for the clear write-up of the root cause — it matched cause 3 in #173 exactly.

🤖 Generated with Claude Code

@mortenaslo mortenaslo closed this Nov 6, 2026
mortenaslo added a commit that referenced this pull request Nov 6, 2026
…st (#173)

* fix(sidebar): drag-and-drop lands where you drop, and shows where first

Reported as "the session doesn't land where I want it, and it's hard to
know where it actually lands", most noticeably with groups and a PS session
next to a Claude one. A faithful simulation of the old code reproduced it
in 7 of 7 scenarios. There was not one cause but five, and one more
mechanism that undid even correct drops.

The drop was resolved to an INDEX, computed in one list and applied to two
others that hold different sessions:

1. The index counted the visible rows, so a "⎇ repo (N)" worktree cluster
   header shifted it by one.
2. In FilterStrip mode it was an index into the FILTERED rows, applied to
   SessionManager's full list (every group): dropping into the middle of a
   group put the session at the top.
3. In inline-headers mode it was a _vm.Sessions index (live only), applied
   to SessionManager (live + dormant): a sleeping session above the target
   moved the drop.
4. The same index moved both lists, so the pane grid and the sidebar went
   out of step.
5. Remove-then-insert at the pre-removal index made every downward move
   overshoot by one.

A drop now resolves to an anchor ("before session X" / "after session Y"),
which is the same slot in every list. The logic is in a new WPF-free
SidebarDropResolver, fed the rows as actually rendered.
SessionManager.MoveSessionRelative finds the anchor after removing the moved
session, and the index-based MoveSession is gone from both SessionManager and
MainViewModel so the pattern can't come back. The grid is re-aligned to the
sidebar order (SessionOrder.AlignTo) instead of moved by the same index, and
RegisterSession inserts a woken or restarted session by relative position
for the same reason.

Then the part that made "PS and Claude" worse: "Open PowerShell here"
creates a session in the same repo, so it counts as a worktree sibling, and
ApplyClusteredOrder re-sorted EVERY sibling on any Add/Remove and whenever a
repo root resolved, i.e. on every restore. A PS session dragged away from
its Claude session snapped back the next time anything opened or closed,
and the saved order was rewritten on every launch. It also moved
SessionManager by a _vm.Sessions index (cause 3 again). It is replaced by
PlaceNewSessionsInClusters: only sessions launched with restoring: false are
placed, once, when their root resolves, and a drag removes a session from
that set. New PS sessions still appear beside their Claude session; after
that, the user's order wins. Behaviour change: siblings the user has
separated are no longer pulled back together.

"Hard to know where it lands": while dragging, a blue insertion line
(Views/InsertionLineAdorner) shows the slot. It is computed from the same
resolved target the drop applies, so it cannot disagree with the drop. It
hides over group headers, which highlight themselves instead. Dropping on an
inline group header now lands the session at the top of that group, rather
than wherever its old global position fell among the new group's members.

Tests: SidebarDropResolverTests and SessionOrderTests, plus
SidebarReorderScenarioTests, which runs scenarios 1-6 end-to-end through
the real SessionManager, the resolver and the grid alignment. Re-introducing
the overshoot, or a grid that doesn't follow the sidebar, makes 4 tests fail
each. Scenario 7 (restore) is enforced by the restoring: false gate in
LaunchSessionAsync, which unit tests can't reach. Not yet tested manually.
CLAUDE.md gains a "Sidebar order and drag-and-drop" section recording why no
index may cross between the lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX

* test(sidebar): carry over #159's restart/wake placement cases

PR #159 fixed the same RegisterSession index-space bug with its own
SessionOrder.LiveInsertIndex; this branch fixes it with InsertionIndex. Its
six placement cases (dormant above, first/last, several dormant, restore in
progress, nothing dormant) pass unchanged against InsertionIndex, so #159 can
close without losing its coverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants