Skip to content

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

Merged
mortenaslo merged 2 commits into
mainfrom
fix/sidebar-drag-drop
Nov 6, 2026
Merged

mortenaslo merged 2 commits into
mainfrom
fix/sidebar-drag-drop

Conversation

@mortenaslo

Copy link
Copy Markdown
Contributor

Summary

Sidebar drag-and-drop now lands the session exactly where you drop it, in both the sidebar and the pane grid, and a blue insertion line shows the slot while you drag. Also fixes a restarted or woken pane jumping out of its sidebar position — the bug #159 targets — so this supersedes #159, and carries its test cases over.

Why

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 PowerShell session next to a Claude one. A faithful simulation of the old code reproduced it in 7 of 7 scenarios. There were five separate causes, all of the same shape: a drop was resolved to an index computed in one list and applied to others that hold different sessions:

  1. The index counted visible rows, so a ⎇ repo (N) worktree cluster header shifted it by one.
  2. In FilterStrip mode it indexed the filtered rows but was applied to the full list — dropping mid-group put the session at the top.
  3. In inline-headers mode it was a live-only index applied to live + dormant, so a sleeping session above the target moved the drop. (Same root cause as fix(layout): keep a restarted or woken pane in its sidebar position #159's restart/wake bug.)
  4. The same index moved both lists, so grid and sidebar went out of step.
  5. Remove-then-insert at the pre-removal index made every downward move overshoot by one.

And one mechanism undid even correct drops: ApplyClusteredOrder re-sorted every worktree sibling on any add/remove and on every restore, so a PowerShell session dragged away from its Claude session snapped back.

Implementation notes

  • A drop resolves to an anchor ("before X" / "after Y"), the same slot in every list — WPF-free SidebarDropResolver, fed the rows as actually rendered.
  • SessionManager.MoveSessionRelative finds the anchor after removal. Index-based MoveSession is removed from SessionManager and MainViewModel so the pattern can't return.
  • The grid is re-aligned to sidebar order (SessionOrder.AlignTo); RegisterSession inserts a woken/restarted session by relative position (SessionOrder.InsertionIndex).
  • ApplyClusteredOrder → PlaceNewSessionsInClusters: only sessions created in this run are placed beside their siblings, once. Behaviour change: siblings the user has separated are no longer pulled back together.
  • Dropping on an inline group header lands the session at the top of that group.
  • CLAUDE.md: new "Sidebar order and drag-and-drop" section on why no index may cross between the lists.

Test plan

  • SidebarDropResolverTests, SessionOrderTests, SidebarReorderScenarioTests (scenarios 1–6 end to end through the real SessionManager, resolver and grid alignment). Re-introducing the overshoot, or a grid that doesn't follow the sidebar, fails 4 tests each.
  • fix(layout): keep a restarted or woken pane in its sidebar position #159's six restart/wake placement cases, ported, pass against InsertionIndex.
  • Full suite on the combined merge of this with every other pending branch: 669 passing.
  • Manual: drag within a group, across groups, onto a group header, past a sleeping session, in both group display modes.
  • Manual: restart / wake a session with a sleeping session above it — pane keeps its slot.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX

mortenaslo and others added 2 commits October 29, 2026 15:05
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
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
mortenaslo merged commit 5dbc1ef into main Nov 6, 2026
1 check passed
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.

1 participant