{{ message }}
Studio: stop the overlay rail going click-through while it is scrolling - #9150
Merged
Conversation
`Chat UI Tests (banner)` has been red on main since #9132, on the WebKit leg of "Update banner layout, other engines", at 921x534 on New chat: FAIL the card keeps its full width whatever the scrollbar does cardWidth=430 want=448 railGutter=0 scrolls=True FAIL the rail takes pointer input exactly when it scrolls scrolls=True pointerEvents=none Both assertions are from #8367 and both are right. The second one describes a state a user can actually be in, and the shard's own screenshot shows it: the rail is capped, the llama.cpp card is sliced off at the fold, the Unsloth update card is entirely above it, and the rail is `pointer-events: none`, so there is no scrollbar and no drag that brings it back. The suite's reachability checks pass because they call scrollIntoView, which a reader has no way to do. What #9132 changed is not the product, it is the state the suite finds. The banner scripts used to run on the machine and the Studio home the chat and extra shards had already been through; they now get their own runner and a pristine `~/.unsloth/studio`, since boot-studio-api-only.sh wipes only `auth`. The last green artifact is dark-themed with "Chat C" and "Chat B" in Recents; the first red one is default-themed with "No chats yet". Same commit range, different starting state, so the rail is capped now where it was not before. #9132 is correct; it exposed this rather than caused it, and the same failure reproduces on unrelated PR branches. The defect is that `overflowing` is a prediction. `floorRoom > maxHeight` asks whether the cards WOULD fit if they collapsed to the floor that was measured for them under a max-height of 0. When they stop short of that floor the box overflows a cap the prediction says it fits inside, and the rail is left click-through while it scrolls, which is the failing pair exactly. So the reading is kept and the actual state is or-ed into it, taken off the node in the same synchronous block that already lifts and restores the cap, with the real cap back on and the layout just flushed. It is re-read on every pass and never remembered, which is what the derived value was there to protect against: the observers watch the rail and every descendant, so a placement that grows to fit clears the flag on the same pass that applied it. Being an `||`, it can only add the case the prediction misses and can never take pointer input away from a rail that already had it. Verified on this box, WebKit and Chromium, against a build actually served from this tree (`unsloth studio --frontend`; the venv's installed copy is what gets served otherwise, which is worth knowing before trusting a local Studio run): - Broken deliberately, `overflowing` forced false: the full Chromium suite is 1433 checks, 1 failed, and the one is `320x480 at 20px: the rail takes pointer input exactly when it scrolls scrolls=True pointerEvents=none`. - Fixed: 1433 checks, 0 failed. Firefox and WebKit spot: 221, 0 failed each. - Unchanged where the prediction was already right. Sweeping WebKit down the height axis at 921 wide, the rail and the flag agree at every step both before and after: no scroll and `none` at 534 through 364, scroll and `auto` from 360 down, card 448 throughout. The width half is not fixed here, because I could not reproduce it and will not guess at it. 448 - 430 is 18px of scrollbar, but `offsetWidth - clientWidth` reports 0 in the same breath, so on that runner the bar takes width out of the content box without showing up in the gutter the assertion reads. This box never reproduces it: WebKit here keeps the card at 448 whether the rail scrolls or not. `scrollbar-gutter: stable` is the obvious candidate and is deliberately not taken on a guess, since it reserves the gutter permanently and would shift the cards left by the scrollbar width on Chromium, where the card is already correct. Instead both failing assertions now print the measurements that would settle it: the card's transform and border box, its computed and max width, innerWidth and documentElement.clientWidth, the rail's offset, client and content widths, its scroll and client heights, its cap, and the children's heights. Nothing is relaxed; the same two checks fail on the same conditions, with enough attached to say which of the two mechanisms it is. That output is what the next run of this shard is for.
Member
Author
The DOM reading was inserted between the flush of the restored cap and the line that puts the eased transition back, which is the one place the store must keep adjacent: update-banner-flex-priority asserts on it, and the assertion went red on both the Linux and the Windows frontend unit jobs. The reading does not need to sit there. The cap change is already committed under the suppression by the flush above it, and transition is not itself a transitionable property, so no reflow the read forces can hand that cap to an animation. Moving it below the restore keeps the invariant and the reading.
The width half of the banner failure is the card's enter animation, not a scrollbar. The diagnostics added earlier in this branch settled it on the WebKit leg: transform matrix(0.96, 0, 0, 0.96, 0, 12), which is exactly the initial state in components/web/update-banner.tsx, with borderBox 448, cssWidth 448px, railContentW 448 and railGutter 0. 448 * 0.96 is 430.08, and 430 is what the assertion read. Nothing had taken any width; the card had simply not been painted at full size yet. So the assertion was measuring the painted box for a question about layout. getBoundingClientRect includes transforms and offsetWidth does not, so the reading moves to offsetWidth, which a scrollbar taking width out of the rail's content box still shrinks. The painted width is still reported next to it, since the gap between the two is the diagnosis. Checked on chromium, firefox and webkit that this is red exactly when it should be: with the transform applied and no width lost, the old reading fails and the new one passes; with 18px actually taken out of the rail's content box the new one fails on all three, with or without the transform also applied; on a settled healthy box both pass.
Member
Author
|
@codex review |
1 similar comment
Member
Author
|
@codex review |
|
Codex Review: Didn't find any major issues. 🚀 Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
Member
Author
This was referenced Aug 18, 2026
shimmyshimmer
added a commit
that referenced
this pull request
Aug 23, 2026
…ter contract Two fixes to the Playwright suite, both mine to begin with. The corner check measured `fromRight` off the rail's border box. The rail carries the shadow gutter, `-mx-3 px-3`, so its border box sits 4px from the viewport edge while the cards it pads sit at 16. It reported 4 and would have failed at every inspected viewport. Measured at 320x480 and at 921x534, one of the two the pass runs: rail 4, every in-flow card 16. It now takes the right edge from the cards, filtered to the ones in flow, since a dragged loaded models card is `fixed` somewhere else. The bottom still comes off the rail: no vertical margin or padding, and its border box does not move when the cards scroll. Checked at both scroll extremes. The other is a contract this branch already invalidated. The suite asserted the rail takes pointer input exactly when it scrolls, which was #9150 and needed the JS that also placed it. The rail is click-through in every state now, so that check failed whenever the rail scrolled. Verified at 320x480 with the rail actually scrolling, scrollHeight 501 against clientHeight 448: pointer-events none, and a click on the gutter lands on the thread viewport behind it. Both assertions are now unconditional.
shimmyshimmer
added a commit
that referenced
this pull request
Aug 25, 2026
) * Studio: put the notification rail back in its bottom-right corner The rail was anchored with `fixed bottom-4 right-4` until #8082, which added the loaded models indicator and, with it, a frame store the rail read: the Live monitor, the API monitor panel and the chat composer publish their boxes, and `useStackGeometry` wrote `bottom` and `maxHeight` as inline styles that dodged them. Every input to that placement moves on its own. The composer grows with its input and goes from centred to docked with no resize event, a download row arrives, release notes expand, a banner is dismissed. Each change re-ran the measurement, and when the room fell short the rail lifted over the obstacle at `viewportHeight - frame.top + STACK_GAP`. On the welcome chat that is the cards in the middle of the window; against a maximised monitor the lift clamped at MIN_STACK_ROOM and parked them at the top. It reads as random because the trigger is measurement timing. The store also had to probe its own output, dropping `max-height` to read `scrollHeight` and restoring it with transitions suppressed, because the placement was feeding on the box it had just sized. It went from 184 lines to 701 over nine follow-up PRs, all of them editing the same arithmetic. So this restores the anchor rather than adding another special case. The rail is `fixed bottom-4 right-4` with the CSS cap again, on the named layer rather than the old hard-coded z-index. The frame store keeps its map of boxes, which api-monitor/panel-placement still needs to open the API panel clear of the monitor and the composer, and loses the dodge arithmetic, the measurement loop and the `coverable` licence. The composer still publishes for that panel's sake. The indicator itself is untouched. Known trade-off, and the pre-#8082 behaviour: in a short window a card can sit over the composer's Send button rather than the rail dodging it. The indicator is off by default, draggable and dismissible. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: scroll the rail at its cap instead of spilling cards off screen Review catch, and it reproduces. The cap alone does not contain anything: the cards that cannot shrink lay out past the rail's bottom edge, which is pinned 16px off the floor, so the overflow goes under the bottom of the window with no way to bring it back. At 320x480 with --ui-font-scale 1.25, three cards up: the llama.cpp banner is in flow with its bottom at 497 against a 480px viewport, and the card after it is squeezed to 0px at 505. scrollHeight 501 against clientHeight 448, and overflow-y visible. The Update button is below the fold for good. Earlier sizes missed it because the download panel is the one min-h-0 card in the rail and absorbed the squeeze on its own. Raising the type size puts the two banner floors over the cap and the slack runs out. So the rail scrolls again, in CSS only. No measurement, no placement: the offset and the cap stay where they were. The gutter comes back with it, since overflow-y forces overflow-x and the card shadows clip without it, and stays horizontal so the bottom card keeps sitting on bottom-4. The rail stays click-through. Verified that the fold is still reachable: elementFromPoint over a card returns a node inside it, so the card is the hit target despite the rail being pointer-events-none, and the rail is that node's nearest scrollable ancestor, which is the chain a wheel scrolls. Focus works too: the Update button measured at bottom 481 in a 480px viewport, and focusing it scrolled the rail to 17 and brought it to 464. * Studio: measure the rail's corner off a card, and drop the stale pointer contract Two fixes to the Playwright suite, both mine to begin with. The corner check measured `fromRight` off the rail's border box. The rail carries the shadow gutter, `-mx-3 px-3`, so its border box sits 4px from the viewport edge while the cards it pads sit at 16. It reported 4 and would have failed at every inspected viewport. Measured at 320x480 and at 921x534, one of the two the pass runs: rail 4, every in-flow card 16. It now takes the right edge from the cards, filtered to the ones in flow, since a dragged loaded models card is `fixed` somewhere else. The bottom still comes off the rail: no vertical margin or padding, and its border box does not move when the cards scroll. Checked at both scroll extremes. The other is a contract this branch already invalidated. The suite asserted the rail takes pointer input exactly when it scrolls, which was #9150 and needed the JS that also placed it. The rail is click-through in every state now, so that check failed whenever the rail scrolled. Verified at 320x480 with the rail actually scrolling, scrollHeight 501 against clientHeight 448: pointer-events none, and a click on the gutter lands on the thread viewport behind it. Both assertions are now unconditional. * CI: drop the install and the artifact dir the deleted rail harness owned The vite install in the banner shard exists for playwright_overlay_rail.py and nothing else, so with the harness gone it is dead. Nothing left in studio-ui-smoke.yml touches npm, npx, node_modules or studio/frontend, so on a frontend-dist cache hit it would run a clean `npm ci` for no consumer. logs/playwright-overlay-rail goes with it: it was only ever written by the two steps this branch removed. Job-level setup-node stays. It predates the harness (#9060) and the installer needs node to build the frontend on a cache miss. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Chat UI Tests (banner)has been red on main since #9132, on the WebKit leg of"Update banner layout, other engines", at 921x534 on New chat:
Both assertions are from #8367 and both are right. The second one describes a
state a user can actually be in, and the shard's own screenshot shows it: the
rail is capped, the llama.cpp card is sliced off at the fold, the Unsloth update
card is entirely above it, and the rail is
pointer-events: none, so there is noscrollbar and no drag that brings it back. The suite's reachability checks pass
because they call scrollIntoView, which a reader has no way to do.
What #9132 changed is not the product, it is the state the suite finds. The
banner scripts used to run on the machine and the Studio home the chat and extra
shards had already been through; they now get their own runner and a pristine
~/.unsloth/studio, since boot-studio-api-only.sh wipes onlyauth. The lastgreen artifact is dark-themed with "Chat C" and "Chat B" in Recents; the first
red one is default-themed with "No chats yet". Same commit range, different
starting state, so the rail is capped now where it was not before. #9132 is
correct; it exposed this rather than caused it, and the same failure reproduces
on unrelated PR branches.
The defect is that
overflowingis a prediction.floorRoom > maxHeightaskswhether the cards WOULD fit if they collapsed to the floor that was measured for
them under a max-height of 0. When they stop short of that floor the box
overflows a cap the prediction says it fits inside, and the rail is left
click-through while it scrolls, which is the failing pair exactly.
So the reading is kept and the actual state is or-ed into it, taken off the node
in the same synchronous block that already lifts and restores the cap, with the
real cap back on and the layout just flushed. It is re-read on every pass and
never remembered, which is what the derived value was there to protect against:
the observers watch the rail and every descendant, so a placement that grows to
fit clears the flag on the same pass that applied it. Being an
||, it can onlyadd the case the prediction misses and can never take pointer input away from a
rail that already had it.
Verified on this box, WebKit and Chromium, against a build actually served from
this tree (
unsloth studio --frontend; the venv's installed copy is what getsserved otherwise, which is worth knowing before trusting a local Studio run):
overflowingforced false: the full Chromium suite is1433 checks, 1 failed, and the one is
320x480 at 20px: the rail takes pointer input exactly when it scrolls scrolls=True pointerEvents=none.height axis at 921 wide, the rail and the flag agree at every step both
before and after: no scroll and
noneat 534 through 364, scroll andautofrom 360 down, card 448 throughout.
The width half is not fixed here, because I could not reproduce it and will not
guess at it. 448 - 430 is 18px of scrollbar, but
offsetWidth - clientWidthreports 0 in the same breath, so on that runner the bar takes width out of the
content box without showing up in the gutter the assertion reads. This box never
reproduces it: WebKit here keeps the card at 448 whether the rail scrolls or not.
scrollbar-gutter: stableis the obvious candidate and is deliberately not takenon a guess, since it reserves the gutter permanently and would shift the cards
left by the scrollbar width on Chromium, where the card is already correct.
Instead both failing assertions now print the measurements that would settle it:
the card's transform and border box, its computed and max width, innerWidth and
documentElement.clientWidth, the rail's offset, client and content widths, its
scroll and client heights, its cap, and the children's heights. Nothing is
relaxed; the same two checks fail on the same conditions, with enough attached to
say which of the two mechanisms it is. That output is what the next run of this
shard is for.