🛗 fix: Anchor Message Nav Gestures to One Measured Rib Layout (#15272)

* 🧭 fix: Anchor the Message Rail to One Measured Layout

The rail's magnifier wrote size into normal flow while its rows, flex items
in a scrolling column without `shrink-0`, compressed to their content the
moment the list overflowed. Hovering grew the column's scrollHeight 363→378
and shifted every rib below the pointer, staling the cached centres: the
centre of `m10`'s rib previewed `m12`, and a click in the gaps — half the
rail's area — followed that same wrong id.

Three more measurements came from different origins. The ribs' offsetParent
was the absolutely positioned `nav`, not the static column, so the rail's
auto-centring inherited the chevron's height (34px ≈ 3.5 ribs). `scrubTo`
mapped the pointer's viewport fraction across the whole rib list, ignoring
the column's own scroll, so no drag position landed on the rib under the
pointer. And `currentId` came from the IntersectionObserver, whose topmost
intersecting row is a rib short of the end whenever a `.steer-render`
follows its response in document order while sitting inside it — the rail
pointing above the end while the reader sits at the bottom.

Everything that answers "which rib is the pointer on" now reads one measured
layout in the column's own content space, and "you are here" is a scroll-spy
over entry spans, so at the bottom current lands on the terminus.

Also: a response's row mounts a frame before its first token, which labelled
the rib with nothing and opened an empty preview card; ribs now name the
pending state. Lighting separates the single current mark from the in-view
band instead of hover replacing both. An origin rib mirrors the terminus. And
the column takes one tab stop with arrow-key walking, rather than one per
message — 226 stops in a 200-message thread.

* 🧭 fix: Address Codex Round 1 on the Message Rail

All three findings were real.

An empty preview is not evidence of generation. `buildEntry` returns nothing
for image-, tool-call- and reasoning-only content too, so a reopened thread
labelled those settled rows "Generating" forever. Only the tail of a live
submission earns that wording now; every other text-free row first reads the
text its rendered card already puts on screen, and falls back to an explicit
"No preview" rather than a blank label and an empty preview panel.

A roving tab stop has to travel with focus. Deriving `tabIndex=0` from the
scroll-spy's current rib alone left a second stop behind the moment an arrow
key moved focus, so Tab re-entered the rail it had just left and Shift+Tab
walked backwards into it instead of out.

And a snap point only exists inside the range the container can reach. The
rows carry `scroll-margin-top: 4rem` against `pt-14` of content padding, so
the first entry's raw snap point is -8px; compared unclamped it read as
"there is still something above you" at the top of every conversation. That
kept the up chevron live with nowhere to go — a defect that predates this
branch — and the new origin rib inherited it, rendering as out of view while
the reader sat at the absolute top. `snapPointFor` now clamps, and the jump
helpers share it so they answer that question the same way the chevrons do.

Re-verified against the real layout: the harness now reproduces the 56px
padding and 64px scroll margin exactly, where the previous run had masked
the negative snap.

* 🧭 fix: Address Codex Round 2 on the Message Rail

Two new findings, both real, both introduced by this branch.

The row fallback was reading chrome. An assistant row renders a VISIBLE `h2`
naming the sender, so the fallback added for settled tool-call and image-only
rows handed back "Claude" for a response that had not produced a token yet —
inventing a preview and, worse, masking the pending state the previous commit
had just added. It now reads the row's message body, which is empty for a
freshly mounted response and carries the card text for a settled one.

And the hit test went stale whenever the rail scrolled. Making the rail
wheel-browsable meant the ribs can move without the pointer moving, but the
pointer's position was cached already converted into the column's content
space. The preview — and the id a click in the gaps follows — stayed on the
rib that used to be there. The pointer is stored as a viewport coordinate now
and converted at the moment it is used, with a scroll handler on the column to
redo the test when the ribs move underneath it.

Pending also narrowed to responses. Between sending and the reply's row
mounting, the reader's own turn is the last entry, and a submission in flight
is not evidence that the user's message is the thing being generated.

The three findings repeated from round one were already fixed in 51180fae7b;
codex re-reviews the whole diff rather than the increment. Re-verified each in
a browser against the real layout, including the row chrome this round adds to
the harness.
This commit is contained in:
Danny Avila 2026-08-27 06:45:57 -04:00 committed by GitHub
parent 0d0d7d05bb
commit 3d808dc906
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 1162 additions and 224 deletions

File diff suppressed because it is too large Load diff

View file

@ -1590,6 +1590,7 @@
"com_ui_message_nav_go_to_assistant": "Go to assistant message: {{0}}",
"com_ui_message_nav_go_to_user": "Go to user message: {{0}}",
"com_ui_message_nav_next": "Navigate to next message",
"com_ui_message_nav_no_preview": "No preview",
"com_ui_message_nav_previous": "Navigate to previous message",
"com_ui_message_part_empty": "Message content cannot be empty.",
"com_ui_method": "Method",
@ -1945,6 +1946,7 @@
"com_ui_scroll_left": "Scroll left",
"com_ui_scroll_right": "Scroll right",
"com_ui_scroll_to_bottom": "Scroll to bottom",
"com_ui_scroll_to_top": "Scroll to top",
"com_ui_search": "Search",
"com_ui_search_above_to_add": "Search above to add users or groups",
"com_ui_search_above_to_add_all": "Search above to add users, groups, or roles",