- Shipped
- September 6, 2026 at 12:18 AM UTC
- Author
- Kamo
- Commit
- 0fb0090
The home cluster's default spot moves from the bottom-right corner to the bottom-left, sitting one padding clear of the primary rail's right edge — collapsed at 80px, expanded at 260px, or held open by a hover-peek. The rail's width is the whole difficulty, and the two obvious sources both answer wrong. `--nav-pri-width` is the SHELL's offset, not the rail's width: a hover-peek deliberately floats the rail over the content instead of reflowing it, so the variable stays at 80 while the member is looking at 260px of nav. And NavLayoutContext, which does know, is unreachable from here — the hex-head engine mounts outside NavLayoutProvider (AuthedChrome renders the tool wrapper as a sibling of the nav tree, not a descendant), so a hook into it would throw. So the rail is measured. NavPri carries a `data-nav-pri` marker and readNavRailInset() reads the right edge off its own box, in the same "ask the DOM" idiom readToolRects already uses for the windows the swarm avoids. It answers 0 where no rail is painted, which is the correct left bound there. A ResizeObserver on that box is what keeps the two together. It fires once per frame of the rail's 220ms width transition, and each frame re-targets the home spring, so the heads travel alongside the rail rather than teleporting after it has finished. A coalescing rAF makes several observations in one frame cost one placement. Only the HOME cluster is inset. Every other cluster sits where the member dragged or threw it, and a reflow that shoved those off the rail would be moving heads somebody parked on purpose. clampToViewport's x rule is reordered rather than extended: the right-edge correction runs first and the left bound overrides it, instead of the old left-then-else-right. Identical for every case that fits, and it settles the one that does not — a viewport too narrow to hold both the rail and the swarm now spills the heads off the right rather than sliding them under the nav, where they cannot be reached. It also stops the two bounds fighting each other across resolveCollisions' six iterations. Nothing is stranded by the move: head positions are not persisted, so every member gets the new spot on their next load. The tooltip and context-menu placement helpers already derive from where a head actually is — a head in the left third opens its label rightward on its own — so only their comments needed correcting, since both asserted heads park bottom-right. Verified by the numbers, which is what this change is: ten new assertions in hexheadHomePlacement.test.ts pin the inset in all three rail states, the honeycomb's left bound landing on rail+padding when the seat at q=-1 would otherwise reach over it, a tool window pushing the swarm without the rail losing its hold, and the narrow-viewport rule. 373 tests across app/components/chat and app/components/tools pass; tsc is clean over every file touched; check-ssr-safe and check-i18n-keys are clean.